Bug: when initialArg changes, useReducer should update the state accordingly
TL;DR: After the first render, useReducer doesn't react to changes in the initialArg (second positional) argument. In my opinion, it should update the state accordingly. The actual behaviour in unnecessarily restrictive and forces us to rely on hacks to address the problems it brings.
Let's take the example of a form provider, a component that enables us to make javascript objects easily editable by users through inputs:
// App.js
const users = {
1: {
firstName: 'Paul',
lastName: 'Atreides',
},
2: {
firstName: 'Duncan',
lastName: 'Idaho',
},
};
const App = () => {
const [id, setId] = useState(1);
return (
<>
<div>Pick User</div>
<button onClick={() => { setId(1); }} type="button">User 1</button>
<button onClick={() => { setId(2); }} type="button">User 2</button>
<FormProvider initialValues={users[id]}>
<Editor />
</FormProvider>
</>
);
};
// FormProvider.js
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
default:
throw new Error();
}
};
const FormProvider = ({ children, initialValues }) => {
const [values, dispatch] = useReducer(reducer, initialValues);
const handleChange = useCallback((evt) => {
dispatch({
field: evt.target.name,
type: 'UPDATE_FIELD',
value: evt.target.value,
});
}, []);
return (
<FormContext.Provider value={{ handleChange, values }}>
{children}
</FormContext.Provider>
);
};
// Editor.js
const Editor = () => {
const { handleChange, values } = useContext(FormContext);
return (
<>
<div>First name:</div>
<input
name="firstName"
onChange={handleChange}
value={values.firstName}
/>
<div>First name:</div>
<input
name="lastName"
onChange={handleChange}
value={values.lastName}
/>
</>
);
};
React version: 17.0.2
Steps To Reproduce
- Click on the "User 2" button.
- Notice nothing happens.
Link to code example: https://codesandbox.io/s/wandering-cache-4vwwe?file=/src/App.js.
The current behavior
When clicking on the "User 2" button, nothing happens. As far as I understand, this is by design. I believe you guys were afraid that people would forget to keep the identity of initialArg stable, which would lead to the state being accidentally reset on every cycle. This does make sense, however it makes composition more difficult for developers who know what they are doing and who do want the state to be updated whenever initialArg changes.
Today, here are my two options:
1. Adding a key prop to <FormProvider />
<FormProvider key={id} initialValues={users[id]}>
This will cause the entire component (and its children) to be unmounted/remounted. The useReducer hook will thus initialise from scratch, with the correct initialValues. Whereas this does work, it's pretty bad in terms of performance and I mean ... it's an ugly hack.
2. Dispatch a RESET action whenever initialValues changes
// FormProvider.js
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
case 'RESET':
return action.values;
default:
throw new Error();
}
};
const FormProvider = ({ children, initialValues }) => {
// ...
const isFirstRenderRef = useRef(true);
useEffect(() => {
if (!isFirstRenderRef.current) {
dispatch({
type: 'RESET',
values: initialValues,
});
}
}, [initialValues]);
useEffect(() => {
isFirstRenderRef.current = false;
}, []);
// ...
};
This will indeed trigger the updating of the state. However, it won't happen before the next cycle. It means that there'll be a moment where the <FormProvider> state will not be mirroring the user's selected profile. Should he decide to update a field during this fraction of a second and it will be an absolute, unforgettable disaster.
The expected behavior
This problem wouldn't exist if the useReducer state was updating when changes in initialArg occur. This is also the most natural behaviour. It would do no harm to developers who want the state not to be updated even though initialArg changes (god, who are these guys ?!), as they might use useState, useRef (useReducer(reducer, useRef(initialValues).current), useMemo or just move the variable outside of the component).
Hey nice write up.
I can confirm I am doing something very similar and it took me a week to figure out this was a framework issue. Would appreciate if this was addressed.
I have a similar form. When fetching the initial data from server using RTK Query , initialArg does not updates.
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!
I still have this issue. I end up updating the key of the parent component every time the initial data changes
@tavoyne How did you solve this issue?
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!