Server Actions are asynchronous functions that are executed on the server. Next.js server actions are designed to handle various server-side logic operations, including data fetching, authentication, and performing CRUD operations. While server actions are often associated with form submissions, they are not limited to this use case. You can use server actions for any asynchronous server-side tasks that need to be executed in response to client-side events.
Table of Contents
Introduction of nextjs server actions and mutation
Next.js Server Actions are like remote code executors, are typically asynchronous functions that run on the server. They run on the server in response to client events (like form submissions) and can handle various tasks. Why We Need Server Actions and Mutations
- Fetching Data: Retrieving data from a database or external API.
- Authenticating Users: Managing user authentication and sessions.
- Performing CRUD Operations: Creating, reading, updating, and deleting records in a database.
- Handling Form Submissions: Processing form data and performing server-side validation and operations.
- Server-Side Logic: Handling actions and mutations on the server can offload processing from the client, resulting in better performance and security.
- Database Operations: Server actions can interact directly with a database, ensuring secure data transactions.
- API Simplification: Server actions can simplify API design by consolidating multiple client-side calls into a single server-side process.
Next.js Server Actions offer a powerful mechanism for executing server-side code in response to user interactions. They provide an alternative to traditional API routes, simplifying data mutation and enhancing the user experience.
In React documentation, Server Actions allow Client Components to call async functions executed on the server. A server action can be defined using the React “use server” directive. You can place the directive at the top of an async function to mark the function as a server action, or at the top of a separate file to mark all exports of that file as server actions.
Implement Nextjs Server Actions to handle the form submission
Next.js server actions are a way to handle form submissions on the server. When a form is submitted, the browser sends a request to the server with the form data. The server then processes this data, typically by validating it, performing some mutation (like updating a database), and then sending a response back to the browser.
In Next.js, server-side rendered pages are generated on the server for each request. This means the HTML can vary based on the data and logic executed on the server.
For a form, the initial form structure is identical for all users. However, when a user submits the form, the data is sent to the server, where it undergoes server-side processing such as validation, database updates, or email sending. The server then returns a response tailored to the submitted data and processing results.
Thus, while the form itself is static, the server's response can differ for each user based on their input and the server-side operations
Step for implementing nextjs server actions to handle the form
- Setting up the form in either in client or server component and use the useFormState hook
- Create actions folder with some file to handle the server actions.
- In actions folder with files define server action to hanlde the form submission.
- Validate the form data using zod.
- Display the validation error in form component
In our case we are using supabase, let follow all steps in details
Example of Nextjs server actions for form validation with backend as Supabase
In this tutorial, we’ll walk through how to handle form submissions and validation in a Next.js application using server actions. We’ll create a blog post submission form, validate the form data, handle file uploads, and save the data to a database using Supabase
Step 1: Setting up the form in client component.
First, set up your form in your component. We can have form in both client or server component based on our requirement.
- Client Components: Opt for real-time validation and dynamic updates based on user input.
- Server Components: Choose for faster initial load and better SEO for static forms.
Regardless of component choice, form actions (data mutation) should always be handled on the server for security and data integrity.
The action
attribute should be set to formAction
.
In app/blogs/new/ folder let add two file
- layout.tsx is optional, here in our case add metadata for SEO or we can add other layout releted code
- In page.tsx file we add the form
In app/blogs/new/page.tsx let add form to add new blog post and we are creating client component for form page.
"use client";
import { useFormState } from "react-dom";
import { submitBlogPostAction } from "@/actions/submitBlogPostAction";
import SubmitButton from "@/components/submit-button";
const initialState = {
message: "",
errors: null,
};
const BlogPostFormPage: React.FC = () => {
const [state, formAction] = useFormState<any>(
submitBlogPostAction as any,
initialState
);
return (
<div className="px-12 pt-24 pb-12 min-h-screen max-w-[100rem] mx-auto flex gap-56">
<div>
<h2 className="text-2xl lg:text-4xl mb-4 uppercase pt-12">
Submit a Blog Post
</h2>
<p className="text-xl">
Fill out this form to submit your blog post.</p>
</div>
<div className="mx-auto w-full h-full p-12 rounded-lg border-2 border-gray-500 border-opacity-10 shadow-lg bg-gray-953">
{state?.type === "error" && (
<p className="text-lg mb-2 bg-red-951 border-2 border-gray-300 rounded-md p-2 my-4">
{state.message}
</p>
)}
<form action={formAction}>
<div className="mb-6">
<label htmlFor="title" className="block mb-2">
Title
</label>
<input type="text" id="title" name="title" />
{state?.errors?.title && (
<span id="title-error" className="text-red-600 text-sm">
{state.errors.title.join(",")}
</span>
)}
</div>
<div className="mb-6">
<label htmlFor="content" className="block mb-2">
Content
</label>
<textarea id="content" name="content"></textarea>
{state?.errors?.content && (
<span id="content-error" className="text-red-600 text-sm">
{state.errors.content.join(",")}
</span>
)}
</div>
<div className="mb-6">
<label htmlFor="author" className="block mb-2">
Author
</label>
<input type="text" id="author" name="author" />
{state?.errors?.author && (
<span id="author-error" className="text-red-600 text-sm">
{state.errors.author.join(",")}
</span>
)}
</div>
<div className="mb-6">
<label htmlFor="category" className="block mb-2">
Category
</label>
<input type="text" id="category" name="category" />
{state?.errors?.category && (
<span id="category-error" className="text-red-600 text-sm">
{state.errors.category.join(",")}
</span>
)}
</div>
<div className="mb-6">
<label htmlFor="tags" className="block mb-2">
Tags
</label>
<input type="text" id="tags" name="tags" />
{state?.errors?.tags && (
<span id="tags-error" className="text-red-600 text-sm">
{state.errors.tags.join(",")}
</span>
)}
</div>
<div className="mb-6">
<label htmlFor="imageUrl" className="block mb-2">
Image
</label>
<input type="file" accept="image/*" id="imageUrl" name="imageUrl" />
{state?.errors?.imageUrl && (
<span id="imageUrl-error" className="text-red-600 text-sm">
{state.errors.imageUrl.join(",")}
</span>
)}
</div>
<SubmitButton />
</form>
</div>
</div>
);
};
export default BlogPostFormPage;
Using the useFormState
Hook
We need to import the useFormState
hook from the react-dom
package and this hooks is not part of the official Next.js API. This hook allows us to manage the state of a form and handle form submissions. Here is syntax of useFormState
const [state, setState] = useState<T>(initialState);
In the above example, submitBlogPostAction
is the server action function that will be executed when the form is submitted. The formAction
variable is a function that we can use as the action
attribute in our HTML form element. The useFormState Hook: Streamlined Form Management
- Manages form state (inputs, errors, submission status)
- Simplifies validation (often with Zod)
- Integrates with server actions for server-side processing
- Provides structured error handling
We use the useFormState
hook to initialize form state and handle submissions. It takes two arguments: a server action function (where form validation rules and API submission are defined) and an initial state object. The formAction
function returned by useFormState
is assigned to the form’s action
attribute.
state
- Purpose:
state
represents the current state of the form. It includes information about the form’s submission status, any errors, and messages. - Usage: It is used to track the state of the form fields, any validation errors, and submission feedback to the user. In our above code we have use state to valid the form field.
formAction
- Purpose:
formAction
is the function that handles the form submission. It is responsible for processing the form data, performing validation, and handling any side effects like updating the server or navigating to another page. - Usage: This function is assigned to the
action
attribute of the<form>
element. When the form is submitted, this function is called to process the form data.
We define, submit-button component that we used in our client form client component. Define components/submit-button.tsx
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" aria-disabled={pending}>
{pending ? "Loading..." : "Add"}
</button>
);
}
export default SubmitButton;
Step 2: Create Nextjs Server Actions for form validation
Let define Server actions in Next.js are typically created in the actions/submitBlogPostAction.ts in root directory. In this file, we’ll define our server action function. Here in server action we validate the form and create new blog to the Supabase blog table. This function will be asynchronous and will take two arguments: prevState
and formData
. Once validation is successful, we upload the image to supbase if success then we are upload the data to supabase blog table.
"use server";
import { z } from "zod";
import { createServerActionClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
const MAX_FILE_SIZE = 5000000;
const ACCEPTED_IMAGE_TYPES = [
"image/jpeg",
"image/jpg",
"image/png",
"image/webp",
];
export async function submitBlogPostAction(prevState: any, formData: FormData) {
console.log({ prevState });
console.log(formData.get("title"));
// Define the schema
const schema = z.object({
title: z.string().min(5, "Title must be at least 5 characters long."),
content: z.string().min(20, "Content must be at least 20 characters long."),
author: z
.string()
.min(3, "Author name must be at least 3 characters long."),
category: z.string().min(3, "Category must be at least 3 characters long."),
tags: z.string().optional(),
imageUrl: z
.any()
.refine((file) => file?.size <= MAX_FILE_SIZE, `Max image size is 5MB.`)
.refine(
(file) => ACCEPTED_IMAGE_TYPES.includes(file?.type),
"Only .jpg, .jpeg, .png and .webp formats are supported."
),
});
// Define the scheme for form using zod
const validatedFields = schema.safeParse({
title: formData.get("title"),
content: formData.get("content"),
author: formData.get("author"),
category: formData.get("category"),
tags: formData.get("tags"),
imageUrl: formData.get("imageUrl"),
});
// Error cases
if (!validatedFields.success) {
return {
type: "error",
errors: validatedFields.error.flatten().fieldErrors,
message: "Missing Fields. Failed to Submit Blog Post.",
};
}
// Success case
const { title, content, author, category, tags, imageUrl } =
validatedFields.data;
try {
const fileName = `${Math.random()}-${imageUrl.name}`;
// Upload the image
const supabase = createServerActionClient({ cookies });
const { data, error } = await supabase.storage
.from("storage")
.upload(fileName, imageUrl, {
cacheControl: "3600",
upsert: false,
});
if (error) {
return {
type: "error",
message: "Database Error: Failed to Upload Image.",
};
}
// If image upload successful then upload the data
if (data) {
const path = data.path;
const { data: blogPosts, error: blogPostError } = await supabase
.from("blog")
.insert({ title, content, author, category, tags, imageUrl: path});
console.log({ blogPosts });
if (blogPostError) {
return {
type: "error",
message: "Database Error: Failed to Submit Blog Post." + error,
};
}
}
} catch (e) {
return {
type: "error",
message: "Database Error: Failed to Submit Blog Post." + e,
};
}
// Here the new data we upload, so fetch the latest and avoid using cache
revalidatePath("/");
redirect("/");
}
The line const supabase = createServerActionClient({ cookies });
in a server action is used to create a Supabase client configured to handle server-side actions, particularly with authentication and session management using cookies.
Note: Why we use revalidatePath(“/”);
Sometimes, newly uploaded content doesn’t show on the client side due to caching. To address this, we need to revalidate the data inside the server action by purging the Next.js cache. Instead of manually deleting the .next
folder, you can use:
import { revalidatePath } from "next/cache";
After successfully uploading new content, call revalidatePath("/")
to refresh the cache. Check more on Data Fetching and Caching in Next.js articles on revalidate.
Step 3: Typescript first schema Validation Library Zod
For form validation we use Zod, It is TypeScript-first schema validation with static type inference and this library allow us to perform validation at server. Where we use safeParse you can see it on our code with bold with blue color code.
The .safeParse
is used in our case where we define the form schema for validation, which returns an object containing success
and error
properties. If it gives success then we get data from it and we used this data to create new post on blogs table on supabase. After defining the schema, we can parse the form data using the schema and check for success or errors. We can then display the error messages in the form. We already added all code for form validation, submission to supabase in above file. Here is just an abstract code for
Understanding Nextjs server action Mutations
Mutations are actions that modify data. In REST APIs, they correspond to POST, PUT, PATCH, and DELETE HTTP methods. In GraphQL, mutations are a specific type of operation that alters data. Mutations are essential for any application that needs to allow users to create, update, or delete resources.
Conclusion
By following this tutorial, you should now have a good understanding of how to create server actions and perform mutations in a Next.js application. These concepts are fundamental for building robust and dynamic web applications. Practice by creating more complex actions and mutations to solidify your understanding. Happy coding!
Related blog post on nextjs
- Mastering generateStaticParams() in Next.js 14: Boost Performance and SEO
- Understanding Nextjs Server Actions and Mutations 2024
- Understanding Routing in Next.js
- Understanding Prefetching in Next.js
- Data Fetching and Caching in Next.js