Resetting Controlled Components in Forms

Published
Last updated

If you're starting to leverage some of React's form primitives (e.g. action / formAction and Server Actions), you might run into the same conundrum I did with controlled components.

By default, React will reset a form after a submission [1], however that doesn't really do much to components that manage their own state, the state managed by React will be preserved on the form submit.

This could lead to possibly weird looking form states after submission, or even bugs with previous values being submitted the next time a form is submitted.

However, there's a really neat way to hook into this automatic form reset behavior within your controlled component, and thats by adding a reset event handler!

Here's a small snippet of a controlled component:

1export function MultiSelect({
2 options,
3 label,
4 name,
5 // maybe other props too
6}) {
7 let [inputValue, setInputValue] = useState("");
8 let [selected, setSelected] = useState([]);
9
10 function handleInput(e) {
11 setInputValue(e.target.value);
12 }
13
14 return (
15 <>
16 <input
17 type="hidden"
18 value={selected.join(',')}
19 name={name}
20 />
21 <label>
22 {label}
23 <input
24 type="text"
25 value={inputValue}
26 onChange={handleInput}
27 />
28 </label>
29 <OptionMenu
30 filter={inputValue}
31 options={options}
32 selectedValues={selected}
33 onSelect={setSelected}
34 />
35 </>
36 )
37}
1export function MultiSelect({
2 options,
3 label,
4 name,
5 // maybe other props too
6}) {
7 let [inputValue, setInputValue] = useState("");
8 let [selected, setSelected] = useState([]);
9
10 function handleInput(e) {
11 setInputValue(e.target.value);
12 }
13
14 return (
15 <>
16 <input
17 type="hidden"
18 value={selected.join(',')}
19 name={name}
20 />
21 <label>
22 {label}
23 <input
24 type="text"
25 value={inputValue}
26 onChange={handleInput}
27 />
28 </label>
29 <OptionMenu
30 filter={inputValue}
31 options={options}
32 selectedValues={selected}
33 onSelect={setSelected}
34 />
35 </>
36 )
37}

When this is used within a form that is submitted, the inputValue and selected state values will be preserved, meaning the MultiSelect will probably show you the values instead of an empty state.

The neat way to handle this reset behavior is to add an event listener:

1// within our `MultiSelect` component
2useEffect(() => {
3 let controller = new AbortController();
4
5 document.addEventListener(
6 'reset',
7 () => {
8 setInputValue("");
9 setSelected([]);
10 },
11 { signal: controller.signal }
12 );
13
14 return () => {
15 controller.abort();
16 }
17}, [])
1// within our `MultiSelect` component
2useEffect(() => {
3 let controller = new AbortController();
4
5 document.addEventListener(
6 'reset',
7 () => {
8 setInputValue("");
9 setSelected([]);
10 },
11 { signal: controller.signal }
12 );
13
14 return () => {
15 controller.abort();
16 }
17}, [])

And that's it! React, under the hood, effectively calls formElement.reset() which will trigger our event listener and clear our managed state!

Footnotes:

[1] - While there is a bit of contention in the community on if that's the right behavior