useActionState retains state when using <Activity> — how to reset useActionState while preserving input value #90107
-
SummaryI want to reset the formAction state on remount while preserving the input value, so that users don’t lose what they typed but the server action state starts fresh each time. Additional informationExpected vs Actual Behavior: Expected: Input value persists when toggling visibility. useActionState resets on remount so the formAction starts fresh. Actual: Input value persists ✅ useActionState retains success = true even after closing and reopening the form ❌ Example |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 3 replies
-
|
The core issue is that Solution 1: Key-based reset (simplest)Force the form to remount when the Activity becomes visible by tying a const [resetKey, setResetKey] = useState(0);
// Call this when the Activity becomes visible again
const handleOpen = () => setResetKey(k => k + 1);
<Activity mode={isVisible ? "visible" : "hidden"}>
<MyForm key={resetKey} />
</Activity>Inside Solution 2: Hybrid — key the action, preserve the inputSeparate the form into two components: one that owns the function SearchPanel({ isVisible }: { isVisible: boolean }) {
const [query, setQuery] = useState('');
const [resetKey, setResetKey] = useState(0);
useEffect(() => {
if (isVisible) setResetKey(k => k + 1);
}, [isVisible]);
return (
<Activity mode={isVisible ? "visible" : "hidden"}>
{/* Input state survives because it's outside the keyed boundary */}
<input value={query} onChange={e => setQuery(e.target.value)} />
{/* Action state resets because the key changes */}
<ActionForm key={resetKey} query={query} />
</Activity>
);
}
function ActionForm({ query }: { query: string }) {
const [state, formAction] = useActionState(submitAction, { error: null });
return (
<form action={formAction}>
<input type="hidden" name="query" value={query} />
<button type="submit">Search</button>
{state.error && <p>{state.error}</p>}
</form>
);
}The |
Beta Was this translation helpful? Give feedback.
-
|
This kind of AntiActivty pattern, where you rotate a key to reset instance, as per, https://react.dev/learn/preserving-and-resetting-state#resetting-a-form-with-a-key, should not necessarily be the first way out of Activity. It may be valid if you model certain parts of the UI as being associated with a transaction id. Dropping in the key rotation should be the last escape hatch for UI under Activity.
In the other comment, for example, it looks ok-ish, but it sets state within a useEffect, which is not-recommended, this leads to double rendering by default. In the snippet it looks ok, in a larger component, it might not be so. Also, as it is, you'll see a flash of the success state, because useEffect lets the browser paint. Switching to useLayoutEffect sounds like a way out of that, but again, in an expensive subtree, double rendering and not painting anything at all, will feel slow. And unless, you go down that path of placing AntiActivity everywhere via a custom component, you'd be leaking logic to the parent of your component, to make them aware that they ought to reset one of their children state.
const [state, action, isPending] = useActionState(asyncReducer, initialState)And the asyncReducer itself: async function asyncReducer(prevState, next) {
// Here you can fork into various paths
// for example
if (next.type === 'RESET') {
return {/* some initial state */}
}
// otherwise trigger a server function,
// or any other sort of work, even router methods if you define
// the reducer in closure
}With that we can sketch a solution (I kept the types high level): async function fakeAction(_prev: State, type?: unknown) {
if (type === "RESET") return { success: false };
await new Promise((r) => setTimeout(r, 500)); // server stuff or w.e.
return { success: true };
}And then via useLayoutEffect, dispatch a transition-action when the component is Activity-hidden: const resetRef = useRef(false)
useLayoutEffect(() => {
return () => {
// When the Activity hides...
if (resetRef.current) {
resetRef.current = false
startTransition(() => {
action("RESET");
});
}
};
}, []);
useEffect(() => {
if (state.success) {
console.log("Effect fired because success = true");
resetRef.current = true;
}
}, [state.success]);Alternatively, and actually the happier path, is when you inline the reducer into useActionState, and then you can set the const [state, dispatchAction, isPending] = useActionState(
async (prev, payload) => {
switch (payload.type) {
case 'SUBMIT':
await submitAction(payload.data)
resetRef.current = true; // This part doesn't have to be in an effect
return { success: true }
case 'RESET':
return { success: false }
default:
throw new Error('not implemented')
}
},
{ success: false }
)Edit: fixed the flashing bit, I rushed to answer and had made a mistake :) |
Beta Was this translation helpful? Give feedback.
-
|
Good callout. You're right that key rotation should be a last resort with A better first approach: treat the stale If you genuinely need a fresh form (e.g., a "new transaction" flow), the cleanest way is to model it at the data level: function TransactionForm({ transactionId }: { transactionId: string }) {
const [state, action] = useActionState(submitTransaction, {
transactionId,
status: "idle",
});
// Form resets naturally when transactionId changes
// because the initial state is different
}The parent controls the transaction lifecycle — when a new transaction starts, pass a new And agreed on the |
Beta Was this translation helpful? Give feedback.
useActionStategives you one input channel (the “payload” you pass toaction(payload)).FormDataand commands (RESET), you need a way to discriminate (type check, tagged union, separate function, etc.).I wouldn't say that's "coupling", it is just what reducers are made for, transforming data given a new action, and for that certain logic per action is necessary.
And if we are gonna bring in coupling here, I think we can agree that, a reducer that handles:
SUBMITwithFormDataRESETwith no payloadis less coupled than pushing reset responsibility up into parents via key-rotation, because of the reasons given ab…