Course
Form
Next.js Mastery: From Fundamentals to Full-Stack
Unlock the power of Next.js with this comprehensive course! Starting with the basics, you’ll learn essential skills such as routing, data fetching, and styling. Progress through practical projects, including building your own React Notes app, to gain hands-on experience. Dive into the source code to understand the inner workings and core principles of Next.js. Perfect for developers with a basic knowledge of React and Node.js, this course ensures you’ll be ready to create high-performance full-stack applications efficiently. Join us and master Next.js, backed by industry experts and a supportive learning community.
Server Actions
Introduction
In the previous article, we covered the basic usage of Server Actions in Next.js. This time, we will explore the "standard" usage of Server Actions, such as which APIs and libraries are often paired with them and what you should consider when writing a Server Action.
We will also address common issues developers face when working with Server Actions, including how to implement optimistic updates, handle errors, access Cookies and Headers, and manage redirects.
Working with Forms
Let’s start with how Server Actions handle form submissions and the APIs that are commonly used in this context.
1.useFormStatus
The
useFormStatus
hook is a React hook that returns the status of a form submission. Here’s how it can be used:'use client'// app/submit-button.jsximport { useFormStatus } from 'react-dom'
export function SubmitButton() { const { pending } = useFormStatus()
return ( <button type="submit" aria-disabled={pending}> {pending ? 'Adding' : 'Add'} </button> )}
// app/page.jsximport { SubmitButton } from '@/app/submit-button'
export default async function Home() { return ( <form action={...}> <input type="text" name="field-name" /> <SubmitButton /> </form> )}
Important:
useFormStatus
must be used within a component that is a descendant of a <form>
element, like in the example above. It’s crucial not to use it directly within the form component as this would not work:function Form() { // 🚩 `pending` will never be true // useFormStatus does not track the form rendered in this component const { pending } = useFormStatus(); return <form action={submit}></form>;}
2.useFormState
useFormState
is another React hook that updates state based on the outcome of the form action. Here's how it can be used:For a simple React example:
import { useFormState } from "react-dom";
async function increment(previousState, formData) { return previousState + 1;}
function StatefulForm() { const [state, formAction] = useFormState(increment, 0); return ( <form> {state} <button formAction={formAction}>Increment</button> </form> )}
In Next.js, combining it with Server Actions:
'use client'
import { useFormState } from 'react-dom'
export default function Home() {
async function createTodo(prevState, formData) { return prevState.concat(formData.get('todo')); }
const [state, formAction] = useFormState(createTodo, [])
return ( <form action={formAction}> <input type="text" name="todo" /> <button type="submit">Submit</button> <p>{state.join(',')}</p> </form> ) }
Hands-On Example
Let's combine
useFormStatus
and useFormState
to handle form submissions using Server Actions. The project structure is as follows:cssCopy codeapp └─ form3 ├─ actions.js ├─ form.js └─ page.js
app/form3/page.js
import { findToDos } from './actions';import AddToDoForm from './form';
export default async function Page() { const todos = await findToDos(); return ( <div className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-md"> <h1 className="text-2xl font-bold mb-4">To-do List</h1> <AddToDoForm /> <ul className="mt-4 space-y-2"> {todos.map((todo, i) => ( <li key={i} className="bg-gray-100 p-2 rounded-md">{todo}</li> ))} </ul> </div> )}
app/form3/form.js
'use client'
import { useFormState, useFormStatus } from 'react-dom'import { createToDo } from './actions';
const initialState = { message: '',}
function SubmitButton() { const { pending } = useFormStatus() return ( <button type="submit" aria-disabled={pending} className="bg-black text-white px-4 py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 disabled:opacity-50" disabled={pending} > {pending ? 'Adding...' : 'Add'} </button> )}
export default function AddToDoForm() { const [state, formAction] = useFormState(createToDo, initialState)
return ( <form action={formAction} className="space-y-4"> <div className="flex space-x-2"> <input type="text" name="todo" className="flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Input to-do item.." /> <SubmitButton /> </div> <p aria-live="polite" className="sr-only"> {state?.message} </p> </form> )}
app/form3/actions.js
'use server'
import { revalidatePath } from "next/cache";
const sleep = ms => new Promise(r => setTimeout(r, ms));
let data = ['Reading', 'Writing', 'Meditation']
export async function findToDos() { return data}
export async function createToDo(prevState, formData) { await sleep(500) const todo = formData.get('todo') data.push(todo) revalidatePath("/form3"); return { message: `Added ${todo} successfully!` }}
The interaction works as follows:
useFormState
: The first parameter in the Server Action function isprevState
, and the second isformData
.useFormStatus
: It should be used in a separate component under the form, such as the submit button component.
Additionally, the line:
<p aria-live="polite" className="sr-only"> {state?.message}</p>
aria-live
: It’s an ARIA attribute used to politely notify users of changes.sr-only
: The content is meant for screen readers only, ensuring accessibility.
You should also include the following CSS to hide it visually but keep it accessible to screen readers:
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0;}
Conclusion
This article introduced advanced and "standard" usage of Server Actions, focusing on integrating with form submissions using hooks like
useFormStatus
and useFormState
. These tools are essential for handling form state and submission status effectively in a Next.js application.In the next part, we will dive deeper into more complex scenarios, including optimistic updates, error handling, accessing Cookies and Headers, and managing redirects with Server Actions.