Next.js Discord

Discord Forum

useActionState resets form when its not valid

Answered
Siamese Crocodile posted this in #help-forum
Open in Discord
Siamese CrocodileOP
When submitting this form and its half valid - e.g. email is valid but password and otp is not then the form resets fully and user has to type email again. Why?
'use client';

import { Button } from '@repo/ui/button';
import { Spinner } from '@phosphor-icons/react';
import { Input } from '@repo/ui/input';
import { Label } from '@repo/ui/label';
import { useActionState } from 'react';
import { login } from './_actions/login';

const initialState = {
  email: '',
  password: '',
};

export default function LoginPage() {
  const [state, formAction, pending] = useActionState(login, initialState);

  return (
    <form className='mx-auto w-full max-w-md space-y-4' action={formAction}>
      <div className='grid gap-3'>
        <div className='flex flex-col gap-2'>
          <Label required htmlFor='email'>
            Email
          </Label>
          <Input placeholder='name@example.com' name='email' id='email' />
          {state?.email && (
            <p className='animate-in fade-in mt-1 text-xs font-medium text-red-500'>
              {state.email}
            </p>
          )}
        </div>

        <div className='flex flex-col gap-2'>
          <Label required htmlFor='password'>
            Password
          </Label>
          <Input placeholder='Password' name='password' id='password' />
          {state?.password && (
            <p className='animate-in fade-in mt-1 text-xs font-medium text-red-500'>
              {state.password}
            </p>
          )}
        </div>

      <Button className='w-full'>
        {pending ? <Spinner size={24} /> : 'Login'}
      </Button>
    </form>
  );
}
Answered by joulev
Because the react team decides to. Reason: to mimic no-js behaviour (the page reloading with the form completely reset). It’s a recent change applied to react 19 only.

You have to use onSubmit instead of action if you want to control form reset. Or use next 14 instead of 15.

Don’t ask me why they think this is a good idea. I can’t understand this decision either; and this is a significant obstacle in me upgrading my apps to react 19 + next 15 when they are released.
View full answer

11 Replies

Siamese CrocodileOP
'use server';

import { redirect } from '@/lib/locales/navigation';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(1)
});

export async function login(_: any, formData: FormData) {
  const parsed = schema.safeParse({
    email: formData.get('email'),
    password: formData.get('password')
  });

  if (!parsed.success) {
    return {
      email: parsed.error.formErrors.fieldErrors['email']?.[0] ?? '',
      password: parsed.error.formErrors.fieldErrors['password']?.[0] ?? ''
    };
  }

  redirect('/protected');
}
@Siamese Crocodile When submitting this form and its half valid - e.g. email is valid but password and otp is not then the form resets fully and user has to type email again. Why? ts 'use client'; import { Button } from '@repo/ui/button'; import { Spinner } from '@phosphor-icons/react'; import { Input } from '@repo/ui/input'; import { Label } from '@repo/ui/label'; import { useActionState } from 'react'; import { login } from './_actions/login'; const initialState = { email: '', password: '', }; export default function LoginPage() { const [state, formAction, pending] = useActionState(login, initialState); return ( <form className='mx-auto w-full max-w-md space-y-4' action={formAction}> <div className='grid gap-3'> <div className='flex flex-col gap-2'> <Label required htmlFor='email'> Email </Label> <Input placeholder='name@example.com' name='email' id='email' /> {state?.email && ( <p className='animate-in fade-in mt-1 text-xs font-medium text-red-500'> {state.email} </p> )} </div> <div className='flex flex-col gap-2'> <Label required htmlFor='password'> Password </Label> <Input placeholder='Password' name='password' id='password' /> {state?.password && ( <p className='animate-in fade-in mt-1 text-xs font-medium text-red-500'> {state.password} </p> )} </div> <Button className='w-full'> {pending ? <Spinner size={24} /> : 'Login'} </Button> </form> ); }
Because the react team decides to. Reason: to mimic no-js behaviour (the page reloading with the form completely reset). It’s a recent change applied to react 19 only.

You have to use onSubmit instead of action if you want to control form reset. Or use next 14 instead of 15.

Don’t ask me why they think this is a good idea. I can’t understand this decision either; and this is a significant obstacle in me upgrading my apps to react 19 + next 15 when they are released.
Answer
Siamese CrocodileOP
   onSubmit={(e) => {
        e.preventDefault();
        formAction(new FormData(e.currentTarget));
      }}
so like this?
Yeah
Siamese CrocodileOP
eh
Thanks
so strange
@Siamese Crocodile ts onSubmit={(e) => { e.preventDefault(); formAction(new FormData(e.currentTarget)); }}
Abyssinian
hey i just running into same "error" is this still the approach to use to keep form values from not clearing with useActionState
Asian black bear
The intended solution is to send the draft of the form back to the client so that it gets initialized with those values. Alternative, using onSubmit and manually calling the action is fine if you don't need progressive enhancement.