Server Actions and Mutations: Simplifying Data Mutation

by Liben Hailu
7 mins read

Discover the magic of server actions—special functions that handle data mutation on the server. This guide takes you on a practical adventure, showing how to use them in a pet registration app.


Server actions are asynchronous functions that are executed on the server. They can be directly called either from client components or server components to handle data mutation without configuring the API route handler.

To use server action it is necessary to mark a function inline or at module lavel by using "user server" directive.

Note: Only server actions marked by "use server" at the module level are allowed to be called from client components.

Exploring Server Actions: A Practical Journey

Now, let's apply server actions in practice by creating a simple pet registration application using server actions.

Firt thing first let's clone the repository and get into my-pets directory then checkout to the 0-starting-point branch and run pnpm install or npm install.

This example uses vercel postgres go to vercel.com go to the storage tab and create a new database after that go to the .env.local tab copy the enviroment variables then go to your project create .env file paste the enviroment variables dont forget to add .env to .gitignore.

After configuring all of this if you see something like this you are good to go.

CreatePet Form with Server Action

Now let's update create-pet component

create-pet.tsx
import { sql } from '@vercel/postgres';
export const CreatePet = () => {
    async function createPet(formData: FormData) {
        "use server"
        try {
            const name = formData.get("name") as string;
            const owner = formData.get("owner") as string;
            await sql`
            INSERT INTO pets (Name, Owner)
            VALUES (${name}, ${owner});
          `;
        } catch (e) {
            return { message: "Failed to create pet" };
        }
    }
 
    return (
        <form action={createPet} className="flex gap-4 md:flex-row flex-col">
            <div className="grid md:grid-cols-2 grid-cols-1 gap-4">
                <input
                    type="text"
                    name="name"
                    placeholder="Your pet name"
                    className="input input-bordered w-full max-w-xs"
                />
                <input
                    type="text"
                    name="owner"
                    placeholder="Your name"
                    className="input input-bordered w-full max-w-xs"
                />
            </div>
            <button type="submit" className="btn btn-active">Add Your Pet</button>
        </form>
    );
};
 

Note: Now, this component is a server component, and it functions even if JavaScript is disabled or hasn't loaded yet. Now that we have used the HTML form actions attribute and implemented a server action, let's add pending status and cache revalidation to ensure the server action sends the new UI. Let's get started.

Adding Pending State

lets crate a new client component inside componets/submit-button.tsx

submit-button.tsx
'use client'
import { useFormStatus } from "react-dom"
 
type SubmitButtonProps = {
    btnText: string
}
export function SubmitButton({ btnText }: SubmitButtonProps) {
    const { pending } = useFormStatus()
 
    return (
        <button type="submit" className={`btn btn-active ${pending && "btn-disabled"}`}
        aria-disabled={pending}>
            {btnText}
        </button>
    )
}

Let's substitute the submit button in our create-pet form with the SubmitButton component.

Now, you'll notice a disabled state when the form is submitted, and we are utilizing aria-disabled to ensure the input won't be removed by certain screen readers on disabled,and it will be accessible. Next, let's incorporate form validation and error handling into our form.

Server Side Form Validation and Error Handling

Now, it's time to relocate our server action to actions/pet.ts as we need to transform our form into a client component to display error statuses.

Let's add a Zod schema to our application inside actioins/pet.ts.

actions.ts
const schema = z.object({
  name: z.string().min(1, { message: "Pet name required." }),
  owner: z.string().min(1, { message: "Owner name required." }),
});

Validate the form data and send the errors if there are any

actions.ts
const validatedFields = schema.safeParse({
    name: formData.get("name") as string,
    owner: formData.get("owner") as string,
})
 
if (!validatedFields.success) {
    return {
        errors: validatedFields.error.flatten().fieldErrors,
    }
}

Now let's show the errors on our form.

const [state, formAction] = useFormState(createPet, null);

And now lets render the erros like this.

create-pet.tsx
<p className="text-sm text-red-600 p-2">
  {state?.errors?.name?.map((err) => err)}
</p>

checkout the complete source code here.

Why Server Actions


  • Progressive enhancement server actions function seamlessly on form submit, whether JavaScript is disabled or not yet loaded, and get enhaced when JavaScript becomes available.
  • Server actions can be used for sending result cacheing, revalidation and sending a new UI with on server round trip.
  • Can be used out side the form.
  • Can be used inside client and server component in type safe manner.
  • Can be called easy from form with form action attibute.
  • Good developer experience.