React 19 Actions + RAC `Form` aren't working properly due to automatic form reset
Provide a general summary of the issue here
React 19 actions has behavior (that was added in canary cycle after Form component in RAC and documentation around it) that it automatically resets form after submission to mimic default browser behavior - PR Link.
I spotted two things that it's breaking while using RAC Form:
- This form reset sets the value to
defaultValuethat was initially passed, not to the currentdefaultValueone:
https://github.com/adobe/react-spectrum/blob/455cbf45d0641cc4efa1fbe43a68ef858bc84442/packages/%40react-aria/utils/src/useFormReset.ts#L22-L25
This isn't working correctly with the recommended pattern by React that defaultValue represent canonical state from the server.
- Errors passed from action state to
validationErrorsprop onFormaren't properly used because theFormvalidation state is discarded with form reset event. I'm not 100% sure but I feel like it's here in code:
https://github.com/adobe/react-spectrum/blob/455cbf45d0641cc4efa1fbe43a68ef858bc84442/packages/%40react-aria/form/src/useFormValidation.ts#L46-L48
This breaks the pattern where action returns some additional errors that can be created only from the server. This is also the pattern used in the RAC docs.
🤔 Expected Behavior?
React 19 patterns where you use actions to drive both canonical form state and form errors works correctly. You use defaultValue with the formData to prevent the values from being reset and you can pass errors to Form component and they're properly displayed.
😯 Current Behavior
React 19 automatic reset behavior breaks both form state and errors state
💁 Possible Solution
I'm not sure here
🔦 Context
I'm building an SPA app with React 19 RC version and noticed that actions doesn't really work properly with RAC form handling
🖥️ Steps to Reproduce
I created a reproduction:
https://stackblitz.com/edit/vitejs-vite-edgrdn?file=src/App.tsx,package.json
There're three forms rendered:
- Using actions without RAC, example of how React actions should be used. The error is properly displayed after submission, the value is being keep because of the
defaultValuebeing updated. Obviously this misses RAC a11y stuff and all the goodies - Using actions with RAC, you can observe that the error isn't properly being displayed after submission and the value is being reset despite
defaultValuebeing updated - RAC without actions, using plain state, just to confirm that it works in React 19 and how +- it's supposed to behave
Version
RAC 1.3.1
What browsers are you seeing the problem on?
Firefox, Chrome, Safari, Microsoft Edge
If other, please specify.
No response
What operating system are you using?
MacOS Sonoma 14.5, Windows 11
🧢 Your Company/Team
No response
🕷 Tracking Issue
No response
Thanks for the issue and all the detailed information. Someone will look into this soon. Thank you for your patience.
Had a little look into this today. In terms of reset and defaultValue, it's even more complex. Fields such as NumberField which actually take a number as their value, cannot be restored by React since the form data is all strings, and it's unlikely a user would be able to always know how to parse those strings, which is why we'll sometimes recommend a hidden input that you maintain with the value you want to go to the server.
We listen for reset already in our form elements, as you noted on useFormReset. Right now that directly calls setState with the first value that the field had. We'd need to get the value off corresponding ref to the input, then apply whatever transformation to get it into a state we understand.
We can't rely on tracking the defaultValue as users may update it too much, so we wouldn't want to be responsive to it. But on the flip side, if we try to only track the defaultValue in a ref, then we have to do that in a layout effect, which would be too late for the reset event. We would always be one state behind.
As for the discarded error on reset, I'm really not sure what to do about that one. I don't quite follow how React envisions getting rid of the error or what the purpose of a Reset button in a form would now be. I tried adding a Reset button to your example, it doesn't do anything. It doesn't clear the errors and it doesn't reset to the actual default of empty string.
I both hope and do not hope that whatever is holding up React 19 is not related to this. Hard to predict what we should do when React 19 isn't actually out yet.
Has there been any update on this issue? I'm running into the same problem - my validationErrors are getting cleared before they get a chance to be displayed and the defaultValue has the same problem. Barring a surefire solution, is there at least a good workaround you would recommend? I saw in #7204 that a key can be used but that seems a little janky 😅
There's this thread here, where one suggested way to opt-out on the automatic form reset is to use the server action inside an onSubmit handler, instead of passing it to the action prop directly.
I have encountered a similar issue. Indeed, using onSubmit instead of action can avoid the problem. However, handling both cases—when the form needs to be reset and when it doesn’t—makes things more complicated. You can manually reset the form using requestFormReset() provided by React. However, since it requires a form HTML element, some workaround code is necessary.
Here is a sample code snippet (this version resets the form content):
let form: HTMLFormElement | null = null;
export function SampleForm() {
const initialState = {
success: null,
payload: {
content: "",
},
errors: {},
};
const handleAction = async (
_prev: FormResultType<SchemaType>,
formData: FormData,
) => {
const result = await createSample(formData);
if (result.success) {
// Reset
startTransition(() => requestFormReset(form));
}
return result;
};
const [state, action, isPending] = useActionState(handleAction, initialState);
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
form = event.currentTarget;
const formData = new FormData(form);
startTransition(() => action(formData));
};
return (
<Form onSubmit={handleSubmit} validationErrors={state.errors}>
<div className="mb-2">
<TextField
label="Content"
aria-label="content"
name="content"
defaultValue={state.payload.content}
/>
</div>
<Button type="submit" isPending={isPending}>
Add
</Button>
</Form>
);
}
I don’t think this is an ideal solution. Do you have any better workarounds?
Looked into this again. There are a few issues here that are difficult to solve. The main one is that the native 'reset' event gets fired by React after re-rendering as a result of an action. We use the reset event to clear validation errors, but in this case, they shouldn't be cleared immediately after they are set (e.g. returned by the action). Unfortunately there is no way to distinguish a real reset triggered by the user (e.g. clicking an <input type="reset"> or something programmatically calling form.reset()) from React's post-submission reset (which is a call to form.reset() itself). React does not expose any information that we can query to determine which reset events to ignore. Currently only the native reset event fires, not the onReset prop, but I think this is due to a bug in React, not something that was done intentionally. facebook/react#33630
A secondary issue that's slightly easier to fix is using the current defaultValue when a form is reset rather than the initial value when the component first mounted. Most of our components are internally controlled even when you pass in a defaultValue prop, so we will need to update the internal state when a reset occurs.