When do I must use the spread operator in useReducer?

Tags: ,



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!

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.



Source: stackoverflow