Why Next.js server actions are game-changing!
I don’t know about you, but every time a new Next.js release is announced, I get very excited to see what new features have been released. Nowadays, Vercel is in lock step with React in moving as much as possible to the server, which is very exciting to see! This past week, Vercel announced the release of Next.js 13.4, which included a new paradigm for updating data on the server called Server Actions.
Server Actions are designed to enable server-side data updates, reduce client-side JavaScript, and provide progressively enhanced forms. In this article, we will dive into the fundamentals of Server Actions, explore how to create and invoke them, and discuss the various enhancements and benefits they bring to your Next.js applications.
ℹ️ Note these examples were taken directly from the Next.js documentation website. See below for an example of a multi-step form wizard.
Enabling Server Actions
To enable Server Actions in your Next.js project, you need to set the experimental serverActions
flag in your next.config.js
file:
const nextConfig = {
experimental: {
serverActions: true,
},
};
Creating a Server Action
Creating a Server Action is as easy as creating an asynchronous function with the “use server” directive at the top of the function body. The function should have serializable arguments and a serializable return value, based on the React Server Components protocol. Here’s an example:
async function myAction() {
"use server"
// Your server action logic here
}
You can also use a top-level “use server” directive on top of a file, which is useful if you have a single file that exports multiple server actions, and it’s required if you’re importing a server action in a client component.
"use server"
export async function myAction() {
// Your server action logic here
}
Invoking Server Actions
According to the Next.js documentation website, there are several ways to invoke Server Actions:
- Using the
action
prop: React'saction
prop allows you to invoke a server action on a<form>
element.
export default function AddToCart({ productId }) {
async function addItem(data) {
'use server'
const cartId = cookies().get('cartId')?.value;
await saveToDb({ cartId, data });
}
return (
<form action={addItem}>
<button type="submit">Add to Cart</button>
</form>
)
}
- Using the
formAction
prop: React'sformAction
prop lets you handle form actions on elements such as<button>
,<input type="submit">
, and<input type="image">
. TheformAction
prop takes precedence over the form'saction
.
export default function Form() {
async function handleSubmit() {
"use server"
// Your server action logic here
}
async function submitImage() {
"use server"
// Your server action logic here
}
return (
<form action={handleSubmit}>
<input type="text" name="name" />
<input type="image" formAction={submitImage} />
<button type="submit">Submit</button>
</form>
)
}
Optimistic Updates and Progressive Enhancement
Server Actions can also be used with the experimental useOptimistic
hook to implement optimistic updates, enhancing user experience by making your app appear more responsive. Optimistic updates immediately update the UI to reflect the expected outcome instead of waiting for the server action's response.
'use client'
import { experimental_useOptimistic as useOptimistic } from 'react'
import { send } from './_actions.js'
export function Thread({ messages }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { message: newMessage, sending: true }],
)
const formRef = useRef();
return (
<div>
{optimisticMessages.map((m) => (
<div>
{m.message}
{m.sending ? 'Sending...' : ''}
</div>
))}
<form
action={async (formData) => {
const message = formData.get('message')
formRef.current.reset()
addOptimisticMessage(message)
await send(message)
}}
ref={formRef}
>
<input type="text" name="message" />
</form>
</div>
)
}
- If a Server Action is passed directly to a
<form>
, the form is interactive even if JavaScript is disabled. - If a Client Action is passed to a
<form>
, the form remains interactive, but the action is placed in a queue until the form has hydrated. The<form>
is prioritized with Selective Hydration, so it happens quickly.
// src/components/MyForm.js
'use client'
import { useState } from 'react'
import { handleSubmit } from './actions.js'
export default function MyForm({ myAction }) {
const [input, setInput] = useState();
return (
<form action={handleSubmit} onChange={(e) => setInput(e.target.value)}>
...
</form>
)
}
In both cases, the form is interactive before hydration occurs. Although Server Actions have an additional benefit of not relying on client JavaScript, you can still compose additional behavior with Client Actions where desired without sacrificing interactivity.
Putting it all together
Let’s put this all together to build an example wizard registration flow using server actions to save the data at each step of the flow.
First, we’ll add a page:
// app/register/page.tsx
import RegistrationForm from 'src/components/RegistrationForm'
export default function Register() {
return (
<div>
<h1>Registration</h1>
<RegistrationForm />
</div>
)
}
Next, let’s look at the registration form component:
// components/RegistrationForm.tsx
'use client'
import React, { useState } from 'react';
import {
savePersonalInformation,
saveContactInformation,
saveAccountDetails
} from '../../actions'
import PersonalInformationStep from './PersonalInformationStep'
import ContactInformationStep from './ContactInformationStep'
import AccountDetailsStep from './AccountDetailsStep'
const WIZARD_STEPS = [
{
Component: PersonalInformationStep,
action: savePersonalInformation,
},
{
Component: ContactInformationStep,
action: saveContactInformation,
},
{
Component: AccountDetailsStep,
action: saveAccountDetails,
},
]
export default function RegistrationForm() {
const [step, setStep] = useState(0);
const { Component, action } = WIZARD_STEPS[step]
const handleSubmit = async (data: FormData) => {
await action(data);
if (step < WIZARD_STEPS.length - 1) {
setStep((currentStep) => currentStep + 1)
} else {
// Submit multi-step wizard form and navigate
}
}
return (
<Component onSubmit={handleSubmit} />
)
}
Then we add our server actions for the purposes of updating the database at each step of the wizard:
// actions/index.ts
'use server'
import { cookies } from 'next/headers'
import prisma from 'prisma'
import { hashPassword } from 'src/utils/auth'
export async function savePersonalInformation(data: FormData) {
const user = await prisma.user.create({
data: {
firstName: data.get('firstName').toString(),
lastName: data.get('lastName').toString(),
},
})
cookies().set('userId', user.id)
}
export async function saveContactInformation(data: FormData) {
// Save contact information to the database
const id = cookies().get('userId')?.value
await prisma.user.update({
where: { id },
data: {
email: data.get('email').toString(),
phone: data.get('phone').toString(),
},
})
}
export async function saveAccountDetails(data: FormData) {
// Save account details
const id = cookies().get('userId')?.value
const password = await hashPassword(
data.get('password').toString()
);
await prisma.user.update({
where: { id },
data: {
username: data.get('username').toString(),
password,
},
})
}
Next we will add a Wizard step component that includes a form that handles submission at each step of the wizard:
// components/WizardStep.tsx
import React from 'react'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
type Props = {
title: string;
onSubmit: (data: FormData) => Promise<void>
children: React.ReactNode
}
export default function FormStep({ title, onSubmit, children }: Props) {
const { pending } = useFormStatus();
return (
<form
action={async (formData) => {
await onSubmit(formData);
}}
>
<h2>{title}</h2>
{children}
<button
type="submit"
className={pending ? 'button-submitting' : 'button'}
>
Next
</button>
</form>
)
}
Finally, we create our steps making use of our components, for example:
// ./PersonalInformationStep.tsx
import React from 'react'
import WizardStep from '../WizardStep'
type Props = {
onSubmit: (data: FormData) => Promise<void>
}
export default function PersonalInformationStep({ onSubmit }: Props) {
return (
<WizardStep title="Personal Information" onSubmit={onSubmit}>
<label htmlFor="firstName">First Name:</label>
<input type="text" id="firstName" name="firstName" required />
<label htmlFor="lastName">Last Name:</label>
<input type="text" id="lastName" name="lastName" required />
</WizardStep>
)
}
This is definitely a contrived example, but imagine if after step one you wanted to show the name of the user in the header of the wizard form. You could do this without blocking the user using optimistic updates.
Why server actions are game-changing
Before we conclude, let’s take a moment to discuss why Next.js Server Actions are such a powerful addition to the Next.js ecosystem and how they are poised to redefine web development best practices.
- Reduced Client-side JavaScript: Next.js and React are moving in the direction of minimizing the client side bundle by offloading logic to the server. Next.js has enabled us to move a lot of code to the server, but we were still mostly building forms on the client. That changes with server actions!
- Enhanced user experience: With features like optimistic updates and progressive enhancement, developers can create applications that feel more responsive and seamless. Users no longer need to wait for server responses to see the effects of their actions, and forms remain interactive even without JavaScript, leading to a better overall user experience even before the client hydrates.
- Great developer experience: Server Actions make it seamless to offload data updates to the server, which would’ve required an API in the past. The APIs that Next.js and React provide make it really easy to work with.
Conclusion
In summary, Next.js Server Actions are a game-changing feature that has the potential to revolutionize the way web developers approach server-side data mutations and form handling. By offering a more responsive, scalable, and streamlined development experience, Server Actions are poised to become an indispensable tool for modern web development.