NextJS 13 brought major changes to the framework. One of the newly added features in Server actions. They have taken inspiration from frameworks like remix. In this tutorial, we will be going through the top 5 features of Server actions by creating a NextJS application from scratch. At the time of writing this article Server actions are still in alpha, so not everything will work smoothly.
Before proceeding with this tutorial, the following things are required:
Let's quickly setup the project before diving into server actions. Run the following command to setup a NextJS project:
npx create-next-app@latest --experimental-app
This will prompt you for some inputs regarding the project. You can leave them with the default values except for the prompt regarding src directory, you can opt out of creating the src directory. Once it is installed you can open the project in your favorite editor.
Click this link to download Pocketbase. Extract the zip file, and open Pocketbase in your terminal. Run the following command to start Pocketbase.
./pocketbase serve
Open the Pocketbase admin by opening this link in your browser http://127.0.0.1:8090/_/.You should be able to see the following screen:
Create the admin account and login to Pocketbase admin. Let's create a collection called Cats. Create a collection by clicking on the New Collection button.
Let’s add 2 text fields, and a number to the Cats collection called name, breed, and like as shown below.
Next, update the permission to the collection, by clicking on the lock icons beside all the rules, so that anyone can access the collection as shown below:
We can use the Pocketbase SDK to interact with the database. Run the following command from the root of your application to install the Pocketbase SDK:
npm i pocketbase
Since we have the database ready we can start using it in our code. You can download and add the cat.css file to your app folder and import it into your app/layout.js file as shown below. This file contains the CSS that we will be using in this tutorial. We won’t be covering the CSS that is used in this tutorial.
import "./cat.css";
In your app/globals.css file remove any styles that are added to the body tag as this might affect the pages we will be working on.
Now let's explore the top 5 features of Server actions in NextJS. Let’s start by creating a few routes in our application. We will be adding the following routes
/cat — view all the cats, and add a cat
/cat/[id] —edit a single cat
Under the apps folder create a folder called cat. Then create a file called page.js inside it. This will create the /cat route. We need to export a React component from this file. This component will be used by NextJS to render the /cat page. We will add it later in the tutorial and let it be empty for the time being. Create a folder called [id] inside the cat folder. Then create another empty page.js file inside it. This will create the /cat/[id] route. If you are familiar with older versions of NextJS you might find this new. You can run the application with the npm run dev as we are making changes in this tutorial.
Let's explore how to handle form submissions using server actions. In a typical React application or older versions of NextJS, it is not possible to write any backend logic directly in the React component. Usually, the frontend will capture the data from a form and will send the data to the backend through an API call. Using NextJS server actions we can directly write the backend code in the react component itself.
Add the following to the cat/page.js file:
Let’s go through the above code.
const pb = new PocketBase(“http://127.0.0.1:8090");
pb.admins.authWithPassword(“”, “”);
Here we are connecting the Pocketbase database. Update the user name and password with the credentials you used to login to Pocketbase admin.
const records = await pb.collection(“Cats”).getFullList();
Here we are querying the Cats collection to get all the entries in the collection. The Pocketbase SDK provides an ORM-like tool to query the DB.
The rest of the component is fairly simple. We have a form that has two text fields and a submit button through which we can add a cat, and we are listing the cats that are present in the DB. If you open http://localhost:3000/cat in your browser you should see the following:
Let’s add a function to handle the submit button. Add the following function to the react component:
async function addCat(data) {
"use server";
await pb.collection("Cats").create(data);
}
Any function in the react component that handles server-side code should start with “use server” in the function body. In the function, we are adding an entry into the addCat collection. Add this function to the action attribute of the form tag as shown below:
Now the addCat() function will be called when submitting the form. You should be able to add a Cat to the Cats collection when you press the submit button. Under the hood, NextJS will make a POST API call to communicate with the server.
If you are getting an error regarding experimental server actions. Update your next.config.js with the following and restart the server:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
},
};
module.exports = nextConfig;
You will notice that after adding a cat the list doesn’t show the updated data. We can fix that by importing a function called revalidatePath as shown below:
import { revalidatePath } from "next/cache";
and calling that function after submitting the data as shown below:
async function addCat(data) {
"use server";
await pb.collection("Cats").create(data);
revalidatePath("/cat"); // <--------------------- add the function here
}
Now if you submit the form the page will revalidate the path and update the server component. Note that this will only fetch the data and will not reload the entire page. After you add a cat, the page should look like the following:
Notice the table has an entry now. The edit button won’t work as of now since we haven’t added anything to that page yet.
We can have multiple actions in our form. Currently, we are calling addCat() when submitting the form. We can also trigger actions on a button press by giving the button a formAction property. Let’s try this on the edit cat page (in the /cat/[id] route). This is referred to as a dynamic route in NextJS. Since we are passing a dynamic id in the URL as a parameter. Paste the following code into the app/cat/[id]/page.js file.
The above code is similar to what we were doing in the /cat route, however, there are some differences.
const record = await pb.collection(“Cats”).getOne(params.id);
Here we are getting the id from the route parameter and using that to fetch a single record from the DB.
redirect(“/cat”);
Instead of revalidating the current page after submitting the form like how we were doing before, in this case, we are redirecting to the /cat route after updating the current cat. Now if you press the edit button in the /cat route you will be redirected to the new page that we created. The page should look something like this:
When you press the update button it should update the data in the DB and redirect to the /cat route. You will notice that there is a second button in the form called like. Let’s update the like count for the cat when pressing the button. Remember that this is the second button in this form so we will be using formAction property in the button to trigger the action.
Add a new function called likeCat() with the following content:
async function likeCat() {
"use server";
await pb.collection("Cats").update(params.id, {
like: parseInt(record.like) + 1,
});
revalidatePath(`/cat/${params.id}`);
}
Import revalidatePath and add this function to the formAction property of the like button as shown below:
await pb.collection(“Cats”).update(params.id, { like: parseInt(record.like) + 1, }); revalidatePath(`/cat/${params.id}`);
Here we are updating the like count in the DB and revalidating the current route.
On pressing the button you will notice that the like count is increasing.
So far we have added the server action in the server component itself. But it is possible to write the server actions in a separate file and then import them into the server component. It is useful to keep the actions separate as we can share them between different components.
Create a app/cat/actions.js file. Let’s extract the addCat() function into this file and then import it. Paste the following code into the file:
"use server";
import { revalidatePath } from "next/cache";
import PocketBase from "pocketbase";
const pb = new PocketBase("http://127.0.0.1:8090");
pb.admins.authWithPassword("", "");
export async function addCat(data) {
await pb.collection("Cats").create(data);
revalidatePath("/cat");
}
The one major difference to note here is the “use server”; directive at the top which will convert all the functions on this page to server actions.
Now let’s import the function into the app/cat/page.js file and use the function as shown below:
You will notice that the page works exactly the same as before.
One important thing to note here is that the actions only work on forms and elements inside the form because server components cannot handle interactions like button clicks.
It is possible to trigger server actions from inside client/regular components. To demonstrate this let's extract the like button from the app/cat/[id]/page.js file into a separate file and trigger the server action from that. Create a file called actions.js under app/cat/[id] folder and add the following code to it:
"use server";
import { revalidatePath } from "next/cache";
import PocketBase from "pocketbase";
const pb = new PocketBase("http://127.0.0.1:8090");
pb.admins.authWithPassword("", "");
export async function likeCat(id, currentCount) {
await pb.collection("Cats").update(id, {
like: parseInt(currentCount) + 1,
});
revalidatePath(`/cat/${id}`);
}
Like the previous action file, we have used the “use server”; directive to create server actions. We have written a likeCat() function that accepts the id and the current like count of the cat to update the like count in the database.
Create a file called app/cat/[id]/Like.js and paste the following code to it:
“use client”;
In NextJS to declare a client-side component we have to use this directive.
import { useTransition } from “react”;
import { likeCat } from “./actions”;
We have imported the useTransition hook from react, and the likeCat() server action into this component. useTransition is essential for calling the server action because it allows us to trigger the server action without blocking the UI. What it means, in this case, is that it will execute the server action, then the NextJS router will reload the server component and update the state in a single round trip.
To execute the server action we have wrapped the server action inside startTransition() function.
Let’s modify the code in app/cat/[id]/page.js file so that we can use the Like component we have just created as shown below:
You will notice that the page will work exactly the same as before but with the use of server actions in the client-side component. Implementing this same flow would have been very complex without server actions. We would have needed to use useEffect and other hooks. Using server components will allow us to reduce the number of useEffects we write in our component.
Actions also support optimistic updates. You might have noticed one minor issue with the current implementation of the Like component is that it takes a few milliseconds to update the count in the UI. There is something called optimistic updates which will make the component appear as if the server has zero latency. To demonstrate this let's reimplement our like button with an experimental hook called useOptimistic. Create a new file called app/cat/[id]/LikeOptimistic.js and paste the following code into it:
Let’s go through the code.
import { experimental_useOptimistic as useOptimistic } from “react”;
We have imported the experimental_useOptimistic hook. This hook is like a combination of useReducer and useTransition in React. It will update the UI instantly with what you think the UI should look like and will sync up with the true value that the server provides once the response from the server has been received whether it succeeds or fails.
let [optimisticLikes, addOptimisticLikes] = useOptimistic(
{ likeCount: record?.like, sending: false },
(state, newLikeCount) => ({
...state,
likeCount: newLikeCount,
sending: true,
})
);
Here we are passing the likeCount from the server component along with a property called sending so that we will know when the actual update is taking place. Then we declared a reducer function that takes the previous state and updates it with the current value. optimisticLikes will have the current value of likeCount and sending, and addOptimisticLikes is used to change the value of the state.
Here we are rendering the value of likeCount, followed by conditionally rendering a sending message that indicates when the update is in progress.
Finally, in the button, we are calling addOptimisticLikes with the updated like count, then we are calling the server action to update the data in the DB.
Let’s add the LikeOptimistic component alongside the Like component to see the difference. You should see the output similar to the following:
You will notice that optimisticLikes (in the h3 tag) will instantly increment the count while there is a delay in the like component. This will make your server look extremely fast even though it is not.
In conclusion, NextJS 13’s server actions empower developers to create dynamic and interactive web applications with ease. By providing features like multiple actions in the same form, optimistic updates, and composed server actions, NextJS 13 equips us with the tools needed to build powerful, efficient, and user-friendly applications. So, embrace the power of server actions in Next.js 13 and unlock new possibilities in your web development journey. Happy coding!
You can refer to the full source code for the above in this repo.
Let's collaborate to turn your business challenges into AI-powered success stories.
Get Started