Why Next.js server actions are game-changing!

Why Next.js server actions are game-changing!

Tags
React.js
Next.js
Web Dev
Published
May 6, 2023
Link
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, except for the examples in the “Putting it all together section”, the code included in this article was borrowed from the Next.js documentation website.

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 }
In fact, you can include this right in your client component!
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:
  1. Using the action prop: React's action 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> ) }
  1. Using the formAction prop: React's formAction prop lets you handle form actions on elements such as <button>, <input type="submit">, and <input type="image">. The formAction prop takes precedence over the form's action.
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> ) }
  1. If a Server Action is passed directly to a <form>, the form is interactive even if JavaScript is disabled.
  1. 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.
  1. 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!
  1. 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.
  1. 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.