Course
Server Actions II
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
When developing Server Actions in Next.js, there are several critical aspects to keep in mind to ensure your code is both functional and maintainable. Here are the main points to focus on:
1. Retrieving Data
When working with Server Actions, the way you retrieve data depends on how the action is invoked:
- Form Actions: If using a form action, the Server Action function receives
formData
as its first parameter:
export default function Page() { async function createInvoice(formData) { 'use server' const rawFormData = { customerId: formData.get('customerId') } // mutate data // revalidate cache } return <form action={createInvoice}>...</form>}
- Form Action with
useFormState
: If using a form action withuseFormState
, the Server Action function receivesprevState
as the first parameter andformData
as the second:
'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> ) }
- Direct Invocation: If you directly invoke the Server Action, the parameters depend on how the function is called. For example:
'use client'
import { createToDoDirectly } from './actions';
export default function Button({ children }) { return ( <button onClick={async () => { const data = await createToDoDirectly('Workout'); alert(JSON.stringify(data)); }}> {children} </button> );}
'use server'
export async function createToDoDirectly(value) { const form = new FormData(); form.append("todo", value); return createToDo(form);}
2. Form Validation
Next.js recommends using HTML's native validation attributes (e.g.,
required
, type="email"
) for basic form validation.For more advanced server-side validation, you can use schema validation libraries like
zod
:'use server'
import { z } from 'zod'
const schema = z.object({ email: z.string().email("Invalid Email"),})
export default async function createsUser(formData) { const validatedFields = schema.safeParse({ email: formData.get('email'), })
if (!validatedFields.success) { return { errors: validatedFields.error.flatten().fieldErrors, }; }
// Mutate data}
3. Revalidating Data
After mutating data with Server Actions, it's crucial to revalidate the relevant data to ensure the UI reflects the latest state. You can use
revalidatePath
or revalidateTag
:- Using
revalidatePath
:
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost() { try { // Mutate data } catch (error) { // Handle error }
revalidatePath('/posts');}
- Using
revalidateTag
:
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost() { try { // Mutate data } catch (error) { // Handle error }
revalidateTag('posts');}
4. Error Handling
Handling errors in Server Actions is essential for providing a smooth user experience. There are two main approaches:
- Returning Error Messages: If an operation fails, you can return an error message that can be displayed on the client:
'use server'// app/actions.jsexport async function createTodo(prevState, formData) { try { await createItem(formData.get('todo')); return revalidatePath('/'); } catch (e) { return { message: 'Failed to create' }; }}
On the client side, display the error message:
'use client'// app/add-form.jsximport { useFormState, useFormStatus } from 'react-dom';import { createTodo } from '@/app/actions';
const initialState = { message: null,};
function SubmitButton() { const { pending } = useFormStatus();
return ( <button type="submit" aria-disabled={pending}> Add </button> );}
export function AddForm() { const [state, formAction] = useFormState(createTodo, initialState);
return ( <form action={formAction}> <label htmlFor="todo">Enter Task</label> <input type="text" id="todo" name="todo" required /> <SubmitButton /> <p aria-live="polite" className="sr-only"> {state?.message} </p> </form> );}
- Throwing Errors: Alternatively, you can throw errors that will be caught by the nearest
error.js
file in the component tree:
'use client'// error.jsexport default function Error() { return ( <h2>Error Occurred</h2> );}
// page.jsimport { useFormState } from 'react-dom';
function AddForm() { async function serverActionWithError() { 'use server'; throw new Error(`This is an error in the Server Action`); }
return ( <form action={serverActionWithError}> <button type="submit">Submit</button> </form> );}
export default AddForm;
When the Server Action encounters an error, the error UI will be displayed.
Conclusion
Server Actions in Next.js are powerful tools for managing data mutations, but they require careful handling of form data, validation, revalidation, and error management. By following these guidelines, you can create robust and user-friendly applications that make full use of Server Actions.