DevLog;

Next.js Server Actions: Beware the Hidden Form Reset

Cover Image for Next.js Server Actions: Beware the Hidden Form Reset
Milan Medvec
Milan Medvec

Over the last few weeks I’ve been experimenting with Next.js Server Actions and the new useActionState hook. Overall, the DX has been great—cleaner form handling, no client mutations, and beautifully simple server code.

But then I ran into something… unpleasant. Every time I submitted my form, the browser triggered a reset event.

How to reproduce it?

Create Next.js app:

npx create-next-app@latest nextjs-form-reset

And create these 3 files:

  • app/state.ts
export interface State {}
  • app/action.ts
"use server";

import { State } from "./state";

export async function action(state: State, formData: FormData): Promise<State> {
    console.log("Action called with: ", formData);

    return state;
}
  • app/page.tsx
"use client";

import { useActionState } from "react";
import { State } from "./state";
import { action } from "./action";

export default function () {
    const [state, formAction, isPending] = useActionState<State, FormData>(action, {});

    return (
        <form action={formAction} ref={form => {
            form?.addEventListener('reset', console.log);
        }}>
            <div>
                <input type="text" name="name" />
            </div>
            <div>
                <input type="checkbox" name="checkbox" />
            </div>
            <div>
                <button type="submit">Submit</button>
            </div>
        </form>
    );
}

When you run the app, and submit the form, you’ll see the following in the browser console:

form reset

Why is it important?

If you don’t know this is happening, it can lead to very confusing behavior. In my case, I had a form using Radix UI components, and I was managing the form state with the useForm hook. When the browser performed a form reset, the React state did not reset, leaving the form in an inconsistent and invalid state. The behavior varied across different fields, which made debugging even harder.

Eventually, I discovered that Radix checkboxes explicitly listen for the reset event, as shown in the source code.

A few other useful references: