I’ve noticed that in many useReducer
examples, the spread operator is used in the reducer like this:
const reducer = (state, action) => { switch (action.type) { case 'increment1': return { ...state, count1: state.count1 + 1 }; case 'decrement1': return { ...state, count1: state.count1 - 1 }; case 'increment2': return { ...state, count2: state.count2 + 1 }; case 'decrement2': return { ...state, count2: state.count2 - 1 }; default: throw new Error('Unexpected action'); } };
However in many of my practices, I removed ...state
and had no issues at all. I understand that ...state
is used to preserve the state of the remaining states, but would a reducer preserve those states already so the ...state
is not needed?
Can someone give me some examples where ...state
is a must and causes issues when removed with useReducer
hook? Thanks in advance!
Advertisement
Answer
No, a reducer function alone would not preserve existing state, you should always be in the habit shallow copy existing state. It will help you avoid a class of state update bugs.
A single example I can think of when spreading the existing state may not be necessary is in the case where it isn’t an object.
Ex: a “count” state
const reducer = (state = 0, action) => { // logic to increment/decrement/reset state };
Ex: a single “status” state
const reducer = (state = "LOADING", action) => { // logic to update status state };
Spreading the existing state is a must for any state object with multiple properties since a new state object is returned each time, in order to preserve all the existing state properties that are not being updated.
Edit 1
Can you give an example when NO shallow copying causing state update bugs?
const initialState = { data: [], loading: false, }; const reducer = (state, action) => { switch(action.type) { case LOAD_DATA: return { ...state, loading: true, }; case LOAD_DATA_SUCCESS: return { ...state, data: action.data, loading: false }; case LOAD_DATA_FAILURE: return { loading: false, error: action.error, }; default: return state; } };
As can been seen in this example, upon a data load failure the reducer neglects to copy the existing state into the new state object.
const [state, dispatch] = useReducer(reducer, initialState); ... useEffect(() => { dispatch({ type: LOAD_DATA }); // logic to fetch and have loading failure }, []); return ( <> ... {state.data.map(...) // <-- throws error state.data undefined after failure ... </> );
Any selector or UI logic that assumes state.data
always exists or is always an array will fail with error. The initial render will work since state.data
is an empty array and can be mapped, but upon a loading error state.data
is removed from state.