Module 3: Server Actions

Mutating Data directly from the Server

What are Server Actions?

Server Actions are asynchronous functions that are executed on the server. They can be called from Server Components or Client Components to handle form submissions and data mutations.

Security Note: Server Actions create a public HTTP endpoint. Always validate inputs and check authorization inside the action.

Defining Actions

1. Inline in Server Components

// app/page.js (Server Component)
export default function Page() {
  async function createPost(formData) {
    'use server' // Directive is crucial
    const title = formData.get('title')
    await db.post.create({ data: { title } })
  }

  return (
    <form action={createPost}>
      <input type="text" name="title" />
      <button type="submit">Create</button>
    </form>
  )
}

2. Separate File (Recommended for Reusability)

// app/actions.js
'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

// Note: prevState is required when using useFormState
export async function createPost(prevState, formData) {
  const title = formData.get('title')
  
  // 1. Validate
  if (!title) return { error: 'Title required' }

  // 2. Mutate
  await db.post.create({ data: { title } })

  // 3. Revalidate Cache
  revalidatePath('/posts')

  // 4. Redirect
  redirect('/posts')
}

Using in Client Components

You can import server actions into Client Components.

// app/ui/button.js
'use client'

import { createPost } from '@/app/actions'

export default function Button() {
  return (
    <button onClick={async () => {
      await createPost(new FormData())
    }}>
      Click me
    </button>
  )
}

Pending States

Use useFormStatus to show loading indicators during form submission.

'use client'
import { useFormStatus } from 'react-dom'

function SubmitButton() {
  const { pending } = useFormStatus()
  
  return (
    <button disabled={pending}>
      {pending ? 'Saving...' : 'Save'}
    </button>
  )
}

Error Handling

Use useFormState (or useActionState in React 19) to handle return values and errors.

'use client'
import { useFormState } from 'react-dom'
import { createPost } from '@/app/actions'

const initialState = { message: null, errors: {} }

export default function Form() {
  const [state, dispatch] = useFormState(createPost, initialState)

  return (
    <form action={dispatch}>
      <input name="title" />
      {state?.errors?.title && <p>{state.errors.title}</p>}
      <button>Save</button>
    </form>
  )
}