The new App Router released in Next.js 13 brings with it a ton of new features that brought the famous framework by Vercel to a whole new level. We have discussed the new App Router in our article Next.js 13: complete guide to Server Components and the App Directory.
What are Next.js Server Actions?
One of the most exciting features is the introduction of Server Actions or Server Functions. Just like the word says, Server Actions are functions that run on the server, but that we can call from the client, just like a normal function. This is a huge step forward for Next.js, as it allows us to run code on the server without having to create an API endpoint. This is a whole new DX for Next.js developers, and it's going to be a game changer.
To be totally honest, this is actually not a Next.js-specific concept: rather, it's a [React.js built-in functionality, which is still in alpha.
What can I do with Next.js Server Actions?
Server Actions are a very powerful feature, and they can be used for a lot of different use cases. Here are a few examples:
- writing to a database: you can write to a database directly from the client, without having to create an API endpoint - just by defining your logic in a server action.
- server logic: executing any server-related business logic, such as sending emails, creating files, etc.
- calling external APIs: you can call external APIs directly from server actions, without having to create an API endpoint
In summary, you can do anything you would normally do on the server, but without having to create an API endpoint.
Pros to using Next.js Server Actions
There are a few pros to using Next.js Server Actions:
- No need to create an API endpoint: you can run server code without having to create an API endpoint.
- Jumping to the definition: you can jump to the definition of a server action just by clicking on it in your code editor, without the need of searching for it in your codebase.
- Type safety: you can use TypeScript to define the arguments and return value of your server actions, and Next.js will automatically validate them for you.
- Less code: you can write less code, as you need a lot less boilerplate to run server code - you can just define a function and its parameters - and then call it from the client.
There's a lot to love about Next.js Server Actions, and I'm sure you'll find a lot of use cases for them.
How to define a Next.js Server Action
Server actions can normally be defined anywhere in your components but with a few exceptions. Let's take a look at a few scenarios.
Before you start, ensure you have enabled the experimental server actions in your next.config.js
file:
module.exports = {
experimental: {
serverActions: true,
}
};
Defining a Server Action in a Server Component
If you are defining a server action in a server component, the only thing you need to do is to define a function with the use server
keyword at the top.
For example, the below is a valid server action:
async function myActionFunction() {
'use server';
// do something
}
Very important: server actions functions should have serializable arguments and a serializable return value based on the React Server Components protocol. This is because the function's response will be serialized and sent to the client.
Defining multiple Next.js Server Actions
An alternative way to define a server action is to use export multiple functions from a file, adding the use server
keyword at the top of the file.
'use server'
export async function myActionFunction() {
// do something
}
export async function anotherActionFunction() {
// do something
}
Client Components can only import actions from server actions files: client components cannot define server actions inline from the same file - but you can still import them from a file that defines multiple server actions using the use server
keyword.
How to invoke a Next.js Server Action
To invoke a server action, you have numerous options.
Invoking a Server Action from a Form
The simplest way to invoke a server action is to invoke it from a form. To do so, you can use the onSubmit
prop of the form
element, and call the server action from there.
export function MyFormComponent() {
function handleFormAction(
formData: FormData
) {
'use server';
const name = formData.get('name');
// do something
}
return (
<form action={handleFormAction}>
<input type={'name'} />
<button type="submit">Save</button>
</form>
);
}
Displaying the form status while the Server Action is running
If you want to display a loading indicator while the server action is running, we need to use a new experimental experimental_useFormStatus
hook.
There are a few things you need to know about this hook:
- It needs to be called within the
form
element - It needs to be called within a
client component
Consider the component below:
'use client';
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
function CreatePostForm() {
return (
<form action={createPostAction}>
<div className='flex flex-col space-y-4'>
<h2 className='text-lg font-semibold'>Create a new Post</h2>
<Label className='flex flex-col space-y-1.5'>
<span>Title</span>
<Input name='title' placeholder='Ex. The best Next.js libraries' required />
</Label>
<Label className='flex flex-col space-y-1.5'>
<span>Description</span>
<Input />
</Label>
<SubmitButton />
</div>
</form>
);
}
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button disabled={pending}>
{pending ? 'Creating article...' : 'Create Article'}
</button>
);
}
As you may have noticed, we are calling the useFormStatus
hook within the form
element, and we are also calling it within a client component.
To make sure the useFormStatus
hook works, we create a new SubmitButton
component, and we call it from the CreatePostForm
component.
Invoking a Server Action from a Button
You can also invoke a server action from a button. To do so, you can use the handleAction
prop of the button
element, and call the server action from there.
export function Form() {
async function handleSubmit() {
'use server';
// ...
}
return (
<form>
<input type="text" name="name" />
<button formAction={handleSubmit}>Submit</button>
</form>
);
}
Invoking a Server Action imperatively
You can also invoke a server action imperatively, by using the useTransition
hook.
First, we define our server action in a separate file that defines multiple server actions:
'use server';
export async function saveData(id) {
await addItemToDb(id);
revalidatePath('/product/[id]');
}
Then, we can import it in our client component, and invoke it imperatively using the useTransition
hook:
'use client';
import { useTransition } from 'react';
import { saveData } from '../actions';
function ClientComponent({ id }) {
let [isPending, startTransition] = useTransition();
return (
<button onClick={() => startTransition(() => saveData(id))}>
Save
</button>
);
}
Revalidating data after a Server Action
You can use the revalidatePath
function to revalidate the data for a specific path. This is useful if you want to revalidate the data for a specific path after the server action has been executed.
Progressive Enhancement
If you use Server Actions from a form
component, these will also work if JavaScript is disabled, as the form will be submitted to the server, unlike actions that are invoked imperatively, which will only work if JavaScript is enabled.
If you can, you're encouraged to use Server Actions from forms, as this will ensure your app works even if JavaScript is disabled.
Server Mutations
Next.js defines Server Mutations as Server Actions that mutate your data and calls redirect
, revalidatePath
, or revalidateTag
.
If you are not doing any of these - you are not doing a mutation, and are not required to use useTransition
: in such cases, you can just call the server action directly from your client components.
Server Actions Error Handling
Any non-trivial application needs to handle errors gracefully - and learning how to do it with Server Actions is paramount.
As we've mentioned above, there are multiple ways to invoke a Server Action: from a form, from a button, or imperatively. Let's take a look at how to handle errors in each of these cases.
Unhandled errors from a Server Action
In all cases above - if you don't handle the error from a Server Action - the error will bubble up to the upper error.tsx
component.
Handling errors from a form
If you are invoking a Server Action from a form, you can wrap your form using an ErrorBoundary
component, and handle the error from there.
Let's create a very basic ErrorBoundary
component that will render a fallback UI if an error occurs:
'use client';
import { Component } from 'react';
class ErrorBoundary<Props extends {
fallback: React.ReactNode;
children: React.ReactNode
}> extends Component<Props> {
state = { hasError: false };
constructor(props: Props) {
super(props);
}
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return this.props.fallback;
}
return this.props.children;
}
}
export default ErrorBoundary;
Let's assume we have a Form using a server action that throws an error:
async function serverActionWithError() {
'use server';
throw new Error(`This is error is in the Server Action`);
}
function FormWithServerAction() {
return (
<form action={serverActionWithError}>
<button>Submit Form</button>
</form>
);
}
export default FormWithServerAction;
We can wrap our form using the ErrorBoundary
component, and handle the error from there:
import ErrorBoundary from './ErrorBoundary';
import FormWithServerAction from './Form';
function FormWithErrorBoundary() {
return (
<ErrorBoundary fallback={<p>Something went wrong</p>}>
<FormWithServerAction />
</ErrorBoundary>
);
}
If you click on the button, you'll see the error message rendered.
Handling errors from an imperatively invoked Server Action
If you are invoking a Server Action imperatively, we can wrap the useTransition
hook in a try/catch
block, and handle the error from there.
Let's assume we have a Server Action that throws an error:
'use server';
export async function serverActionWithError() {
throw new Error(`This is error is in the Server Action`);
}
We can invoke it imperatively from a client component, and handle the error from there:
'use client';
import { useTransition } from 'react';
import { serverActionWithError } from './actions';
function ImperativeServerAction() {
const [pending, startTransition] = useTransition();
return (
<button
disabled={pending}
onClick={() => {
startTransition(async () => {
try {
await serverActionWithError()
} catch (e) {
alert('error');
}
});
}}
>
Click Button
</button>
);
}
export default ImperativeServerAction;
If you click on the button, you'll see the alert
popup with the error message.
Conclusion
In this article, we've seen how to use Next.js Server Actions, and how to invoke them from client components.
You can use these today, but they are still experimental, so you should use them with caution - since the API might change in the future.
The new Next.js Supabase SaaS Starter Kit at Makerkit already makes use of Server Components and App Actions - so you can check it out if you want to see a real-world example of how to use them.