đ Server Actions: Your Personal Chef in the Kitchen
Imagine youâre at a fancy restaurant. You donât cook the food yourselfâyou tell the waiter what you want, and the chef in the kitchen prepares it perfectly. Thatâs exactly what Server Actions do in Next.js!
You (the browser) = The customer Server Action = The chef in the kitchen The form = Your order slip
đł What Are Server Actions?
Server Actions are special functions that run on the server, not in your browser. Think of them as sending a message to the kitchen (server) to do something importantâlike saving your order or checking your identity.
Why Use a Chef (Server)?
| Browser (You) | Server (Chef) |
|---|---|
| Everyone can see what you do | Private & secure |
| Limited power | Full database access |
| Can be tricked | Trusted environment |
Simple Example:
'use server'
async function saveOrder(formData) {
const dish = formData.get('dish')
// Save to database (only server can do this!)
await db.orders.create({ dish })
}
The magic word 'use server' at the top tells Next.js: âRun this in the kitchen, not at the table!â
đ Server Action Forms
Forms are how you send your order to the kitchen. Instead of using complicated JavaScript, you just connect your form directly to a Server Action.
The Old Way (Complicated)
// â Old: Lots of code
function OldForm() {
const handleSubmit = async (e) => {
e.preventDefault()
const data = new FormData(e.target)
await fetch('/api/order', {
method: 'POST',
body: data
})
}
return <form onSubmit={handleSubmit}>...</form>
}
The New Way (Simple!)
// â
New: Clean and easy
async function placeOrder(formData) {
'use server'
const dish = formData.get('dish')
await db.orders.create({ dish })
}
function NewForm() {
return (
<form action={placeOrder}>
<input name="dish" placeholder="What do you want?" />
<button type="submit">Order!</button>
</form>
)
}
The formâs action points directly to your Server Action. Thatâs it! No fetch, no prevent default, no hassle.
đŚ The next/form Component
Next.js gives you a special form component thatâs even smarter. Itâs like having a waiter who knows shortcuts through the restaurant!
import Form from 'next/form'
function SearchPage() {
return (
<Form action="/search">
<input name="query" placeholder="Search..." />
<button type="submit">Find</button>
</Form>
)
}
What Makes It Special?
graph TD A["User Types & Submits"] --> B{next/form Magic} B --> C["Prefetches the page"] B --> D["Updates URL smoothly"] B --> E["No full page reload"] C --> F["⥠Feels instant!"] D --> F E --> F
Regular form: Full page refresh, slow next/form: Smooth navigation, fast
đ Server Action Mutations
Mutation = Changing something. Like when you tell the chef to add extra cheese to your order!
Mutations are Server Actions that change data:
- Add a new user
- Delete a comment
- Update your profile
async function addToCart(formData) {
'use server'
const productId = formData.get('productId')
const userId = formData.get('userId')
// This CHANGES data - it's a mutation!
await db.cart.create({
productId,
userId
})
}
Mutations vs Queries
| Mutation | Query |
|---|---|
| Changes data | Reads data |
| âAdd item to cartâ | âShow me my cartâ |
| âDelete my accountâ | âWhatâs my name?â |
| Creates, Updates, Deletes | Only reads |
đŻ Binding Server Actions
Sometimes you need to give the chef extra information thatâs not in the form. Thatâs where binding comes in!
The Problem
// How do I pass the userId? It's not in the form!
async function deleteItem(formData) {
'use server'
const itemId = formData.get('itemId')
// But wait... which user?? đ¤
}
The Solution: bind()
async function deleteItem(userId, formData) {
'use server'
const itemId = formData.get('itemId')
await db.cart.delete({
userId, // Now we have it!
itemId
})
}
function CartItem({ userId, itemId }) {
// "Bake in" the userId before giving to form
const deleteWithUser = deleteItem.bind(null, userId)
return (
<form action={deleteWithUser}>
<input type="hidden" name="itemId" value={itemId} />
<button>đď¸ Delete</button>
</form>
)
}
Think of binding like pre-filling the order with your table numberâthe waiter already knows where to bring the food!
đŽ The useActionState Hook
What if you want to know:
- Is the chef still cooking? (loading)
- Did something go wrong? (error)
- What did the chef send back? (result)
useActionState gives you all these answers!
'use client'
import { useActionState } from 'react'
function LoginForm() {
const [state, formAction, isPending] = useActionState(
login, // Your Server Action
{ error: null } // Initial state
)
return (
<form action={formAction}>
<input name="email" placeholder="Email" />
<input name="password" type="password" />
{state.error && (
<p style={{color: 'red'}}>{state.error}</p>
)}
<button disabled={isPending}>
{isPending ? 'Logging in...' : 'Login'}
</button>
</form>
)
}
// The Server Action returns new state
async function login(previousState, formData) {
'use server'
const email = formData.get('email')
const password = formData.get('password')
if (!email.includes('@')) {
return { error: 'Invalid email!' }
}
// Try to log in...
return { error: null, success: true }
}
The Three Gifts of useActionState
graph TD A["useActionState"] --> B["state"] A --> C["formAction"] A --> D["isPending"] B --> E["What the server sent back"] C --> F["Use this in form action"] D --> G["true while cooking"]
âł The useFormStatus Hook
Want to show a loading spinner? Disable the button while submitting? useFormStatus is your friend!
'use client'
import { useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending, data } = useFormStatus()
return (
<button disabled={pending}>
{pending ? (
<>âł Sending...</>
) : (
<>đ¤ Submit</>
)}
</button>
)
}
Important Rule!
The button using useFormStatus must be inside the form!
// â
Correct: SubmitButton is INSIDE the form
function ContactForm() {
return (
<form action={sendMessage}>
<input name="message" />
<SubmitButton /> {/* â
Inside! */}
</form>
)
}
// â Wrong: Button outside form won't work
function BrokenForm() {
return (
<>
<form action={sendMessage}>
<input name="message" />
</form>
<SubmitButton /> {/* â Outside - won't know about form! */}
</>
)
}
đ Server Action Revalidation
After the chef makes changes, you need to update the menu board so everyone sees the new items. Thatâs revalidation!
Why Revalidate?
graph TD A["User adds product"] --> B["Server saves to database"] B --> C{But the page shows old data!} C --> D["revalidatePath - Update this page"] C --> E["revalidateTag - Update these items"] D --> F["⨠Fresh data appears!"] E --> F
revalidatePath()
Update a specific page after changes:
import { revalidatePath } from 'next/cache'
async function addProduct(formData) {
'use server'
await db.products.create({
name: formData.get('name'),
price: formData.get('price')
})
// Tell Next.js: "The products page changed!"
revalidatePath('/products')
}
revalidateTag()
Update all items with a specific tag:
import { revalidateTag } from 'next/cache'
async function updatePrice(formData) {
'use server'
await db.products.update({
id: formData.get('id'),
price: formData.get('newPrice')
})
// Update ALL product-related caches
revalidateTag('products')
}
Path vs Tag
| revalidatePath | revalidateTag |
|---|---|
| Updates one specific page | Updates anything with that tag |
/products |
âproductsâ, âcartâ, etc. |
| Simpler to use | More flexible |
đŻ Putting It All Together
Hereâs a complete example combining everything:
// actions.js
'use server'
import { revalidatePath } from 'next/cache'
export async function createTodo(prevState, formData) {
const title = formData.get('title')
if (!title || title.length < 3) {
return { error: 'Title too short!' }
}
await db.todos.create({ title })
revalidatePath('/todos')
return { error: null, success: true }
}
// TodoForm.jsx
'use client'
import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'
import { createTodo } from './actions'
function SubmitBtn() {
const { pending } = useFormStatus()
return (
<button disabled={pending}>
{pending ? 'âł Adding...' : 'â Add Todo'}
</button>
)
}
export function TodoForm() {
const [state, action] = useActionState(createTodo, {})
return (
<form action={action}>
<input name="title" placeholder="New todo..." />
{state.error && <p>{state.error}</p>}
<SubmitBtn />
</form>
)
}
đ Quick Summary
| Concept | What It Does |
|---|---|
| Server Actions | Functions that run on the server |
| âuse serverâ | Marks a function as a Server Action |
| form action | Connects form directly to Server Action |
| next/form | Smart form with navigation features |
| Mutations | Server Actions that change data |
| bind() | Pre-fills extra data for the action |
| useActionState | Gets state, action, and pending status |
| useFormStatus | Shows loading state inside forms |
| revalidatePath | Refreshes a specific page |
| revalidateTag | Refreshes tagged data |
đ You Did It!
You now understand Server Actionsâyour personal kitchen staff that handles all the heavy lifting securely on the server. No more complicated fetch calls, no more manual state management. Just simple, clean, powerful forms!
Remember: The browser is the dining room. The server is the kitchen. Server Actions are your chefs making everything happen behind the scenes! đ˝ď¸đ¨âđł
