Server Actions

Back

Loading concept...

🚀 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 &amp; 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! 🍽️👨‍🍳

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.