Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[React 19] allow opting out of automatic form reset #29034

Open
stefanprobst opened this issue May 9, 2024 · 13 comments
Open

[React 19] allow opting out of automatic form reset #29034

stefanprobst opened this issue May 9, 2024 · 13 comments
Labels

Comments

@stefanprobst
Copy link

Summary

repo: https://github.com/stefanprobst/issue-react-19-form-reset

react 19@beta currently will automatically reset a form with uncontrolled components after submission. it would be really cool if there was a way to opt out of this behavior, without having to fall back to using controlled components - especially since component libraries (e.g. react-aria) have invested quite a bit of time to work well as uncontrolled form elements.

the main usecase i am thinking of are forms which allow saving progress, or saving a draft, before final submit. currently, every "save progress" would reset the form.

@pawelblaszczyk5
Copy link

I think you should return current values from action in such case and update the default value 😃

@glo85315
Copy link

glo85315 commented May 9, 2024

@adobe export issue to Jira project PWA

@officialyashagarwal
Copy link

I think you should return current values from action in such case and update the default value. and return required!

@zce
Copy link

zce commented May 14, 2024

This is very necessary in the step-by-step form, such as verifying the email in the auth form first

@tranvansang
Copy link

Be careful to handle if the action throws an error, your "returning the new default" at the end of the function will be ineffective.

#29090

@chungweileong94
Copy link

chungweileong94 commented May 18, 2024

The automatic form reset in React 19 actually caught me off guard, where in my case, I was trying to validate the form inputs on the server, then return & display the input errors on the client, but React will reset all my uncontrolled inputs.

For context, I wrote a library just for doing server-side validation https://github.com/chungweileong94/server-act?tab=readme-ov-file#useformstate-support.

I know that you can pass the original input (FormData #28754) back to the client, but it's not easy to reset the form based on the previously submitted FormData, especially when the form is somewhat complex, I'm talking about things like dynamic items inputs, etc.

It's easy to reset a form, but hard to restore a form.

@chungweileong94
Copy link

Now that I have played with React 19 form reset for a while, I think this behavior kind of forces us to write a more progressive enhancement code. This means that if you manually return the form data from the server and restore the form values, the user input will persist even without JavaScript enabled. Mixed feelings, pros and cons.

@jazsouf
Copy link

jazsouf commented Jun 1, 2024

what about using onSubmit as well the action to prevent default?

@eps1lon
Copy link
Collaborator

eps1lon commented Jun 1, 2024

If you want to opt-out of automatic form reset, you should continue using onSubmit like so:

+function handleSubmit(event) {
+  event.preventDefault();
+  const formData = new FormData(event.target);
+  startTransition(() => action(formData));
+}

...
-<form action={action}>
+<form onSubmit={handleSubmit}>

--

That way you still opt-into transitions but keep the old non-resetting behavior.

And if you're a component library with your own action-based API that wants to maintain form-resetting behavior, you can use ReactDOM.requestFormReset:

function onSubmit(event) {
  // Disable default form submission behavior
  event.preventDefault();
  const form = event.target;
  startTransition(async () => {
    // Request the form to reset once the action
    // has completed
    ReactDOM.requestFormReset(form);

    // Call the user-provided action prop
    await action(new FormData(form));
  })
}

--https://codesandbox.io/p/sandbox/react-opt-out-of-automatic-form-resetting-45rywk

We haven't documented that yet in https://react.dev/reference/react-dom/components/form. It would help us a lot if somebody would file a PR with form-resetting docs.

@rwieruch
Copy link

rwieruch commented Jun 5, 2024

@eps1lon do you think using onSubmit over action is the right call here? A bit of context:

I am surprised by this new default behavior here, because this forces essentially everyone to use onSubmit over action, because everyone wants to keep their form values intact in case of an (validation) error.

So if this reset behavior is a 100% set in stone for React 19, why not suggest using useActionState then with a payload object then where all the form values in the case of an error are sent back from the action so that the form can pick these up as defaultValues?

@karlhorky
Copy link
Contributor

karlhorky commented Jun 5, 2024

this forces essentially everyone to use onSubmit over action, because everyone wants to keep their form values intact in case of an (validation) error

@rwieruch I'm not sure this is true.

As @acdlite mentions in the PR below, it's for uncontrolled inputs.

It has no impact on controlled form inputs.

Controlled inputs are probably in almost every form case still desirable with RSC (as Sebastian mentions "I will also say that it's not expected that uncontrolled form fields is the way to do forms in React. Even the no-JS mode is not that great.")

Also, this is about "not diverging from browser behavior", as @rickhanlonii mentions in more discussion over on X here:

But it does indeed seem to be a controversial choice to match browser behavior and reset uncontrolled fields.

@rwieruch
Copy link

rwieruch commented Jun 5, 2024

Thanks for the input here @karlhorky and putting all the pieces together. I have seen that this matches the native browser more closely, so I see the incentive for this change. Just wanted to double check here, because I am re-adjusting my teaching material again (my own fault here, because we are still quite early on this :)).

So if I am not using a third-party library for forms or actions, would the following code look good for upserting an entity with form + server action, if I still would want to use the action attribute on the form?

const TicketUpsertForm = ({ ticket }: TicketUpsertFormProps) => {
  const [actionState, action] = useActionState(
    upsertTicket.bind(null, ticket?.id),
    { message: "" }
  );

  return (
    <form action={action} className="flex flex-col gap-y-2">
      <Label htmlFor="title">Title</Label>
      <Input
        id="title"
        name="title"
        type="text"
        defaultValue={
          (actionState.payload?.get("title") as string) || ticket?.title
        }
      />

      <Label htmlFor="content">Content</Label>
      <Textarea
        id="content"
        name="content"
        defaultValue={
          (actionState.payload?.get("content") as string) || ticket?.content
        }
      />

      <SubmitButton label={ticket ? "Edit" : "Create"} />

      {actionState.message}
    </form>
  );
};

And then the action returns the payload in the case of an error, so that the form can show this as the defaultValues, so that it does not reset.

const upsertTicketSchema = z.object({
  title: z.string().min(1).max(191),
  content: z.string().min(1).max(1024),
});

export const upsertTicket = async (
  id: string | undefined,
  _actionState: {
    message: string;
    payload?: FormData;
  },
  formData: FormData
) => {
  try {
    const data = upsertTicketSchema.parse({
      title: formData.get("title"),
      content: formData.get("content"),
    });

    await prisma.ticket.upsert({
      where: {
        id: id || "",
      },
      update: data,
      create: data,
    });
  } catch (error) {
    return {
      message: "Something went wrong",
      payload: formData,
    };
  }

  revalidatePath(ticketsPath());

  if (id) {
    redirect(ticketPath(id));
  }

  return { message: "Ticket created" };
};

EDIT: I think that's something @KATT wanted to point out in his proposal: #28491 (comment)

@pawelblaszczyk5
Copy link

Thanks for the input here @karlhorky and putting all the pieces together. I have seen that this matches the native browser more closely, so I see the incentive for this change. Just wanted to double check here, because I am re-adjusting my teaching material again (my own fault here, because we are still quite early on this :)).

So if I am not using a third-party library for forms or actions, would the following code look good for upserting an entity with form + server action, if I still would want to use the action attribute on the form?

const TicketUpsertForm = ({ ticket }: TicketUpsertFormProps) => {
  const [actionState, action] = useActionState(
    upsertTicket.bind(null, ticket?.id),
    { message: "" }
  );

  return (
    <form action={action} className="flex flex-col gap-y-2">
      <Label htmlFor="title">Title</Label>
      <Input
        id="title"
        name="title"
        type="text"
        defaultValue={
          (actionState.payload?.get("title") as string) || ticket?.title
        }
      />

      <Label htmlFor="content">Content</Label>
      <Textarea
        id="content"
        name="content"
        defaultValue={
          (actionState.payload?.get("content") as string) || ticket?.content
        }
      />

      <SubmitButton label={ticket ? "Edit" : "Create"} />

      {actionState.message}
    </form>
  );
};

And then the action returns the payload in the case of an error, so that the form can show this as the defaultValues, so that it does not reset.

const upsertTicketSchema = z.object({
  title: z.string().min(1).max(191),
  content: z.string().min(1).max(1024),
});

export const upsertTicket = async (
  id: string | undefined,
  _actionState: {
    message?: string;
    payload?: FormData;
  },
  formData: FormData
) => {
  try {
    const data = upsertTicketSchema.parse({
      title: formData.get("title"),
      content: formData.get("content"),
    });

    await prisma.ticket.upsert({
      where: {
        id: id || "",
      },
      update: data,
      create: data,
    });
  } catch (error) {
    return {
      message: "Something went wrong",
      payload: formData,
    };
  }

  revalidatePath(ticketsPath());

  if (id) {
    redirect(ticketPath(id));
  }

  return { message: "Ticket created" };
};

Yup, that’s pretty much it. This way it works the same if submitted before hydration happens

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests