skip to Main Content

Strict Mode is triggering my reducer function twice which is expected.

What I didn’t expect was the returned state to have duplicated elements in it.

What I realise is I’m not using useReducer correctly, but I don’t know how to rewrite my reducer function correctly.

The example below should show 1 timestamp on initial render, then with each click of Add Element it should add 1 additional timestamp.
What is happening (with Strict Mode) is each button click is adding 2 additional timestamps.

import { useReducer } from 'react';

const reducer = (state, action) => {
  if (action.type === 'addElement') {
    let newState = { ...state };
    newState.elements.push({
        id: (new Date()).toString()
    })
    return newState;
  }
}

export default function Test() {
  const [state, dispatch] = useReducer(reducer, {
    elements: [
        { id: (new Date()).toString() }
    ]
  });

  function addElement() {
    dispatch({
        type: 'addElement'
    });
  }

  return (
    <>
        <h2>Elements</h2>
        {[...Array(state.elements.length)].map((e, i) => <div key={i}>
            <span>{state.elements[i].id}</span><br />
        </div>
        )}
        <button type='button' onClick={addElement}>Add Element</button>
    </>
  );
}

2

Answers


    • You are mutating the original state array directly by using .push() method, even though you spread the rest of the state.
    • Instead of using .push(), spread the existing array into new array & add the new elements using [...state.elements, newElement].
    • Update your reducer function accordingly, here is the code snippet how to do the same.
    const reducer = (state, action) => {
      if (action.type === 'addElement') {
        return {
          ...state,
          elements: [...state.elements, { id: new Date().toString() }]
        };
      }
      return state;
    };
    
    Login or Signup to reply.
  1. From "Strict Mode is triggering my reducer function twice which is expected." it is obvious from the code that you are pushing twice into the array as an unintentional side-effect.

    The React StrictMode component re-runs certain lifecycle methods and hooks twice as a way to help you detect logical issues/bugs in your code.

    See Detecting unexpected side effects from the older legacy docs, which do a bit better job of explaining the finer details. I’ve emphasized the relevant point.

    Strict mode can’t automatically detect side effects for you, but it
    can help you spot them by making them a little more deterministic.
    This is done by intentionally double-invoking the following functions:

    • Class component constructor, render, and shouldComponentUpdate methods
    • Class component static getDerivedStateFromProps method
    • Function component bodies
    • State updater functions (the first argument to setState)
    • Functions passed to useState, useMemo, or useReducer

    The reducer function is executed twice, and the issue is that you are directly mutating the current state object. In React we never directly mutate state and props. You should update the code to apply the Immutable Update Pattern, i.e. shallow copy all state, and nested state, that is being updated. Array.push directly pushes into the source array and mutates it.

    You can use either the Spread Syntax to copy the array, or use Array.concat to append to and return a new array reference.

    Examples:

    const reducer = (state, action) => {
      switch(action.type) {
        case 'addElement':
          return {
            ...state,            // <-- shallow copy state,
            elements: [          // <-- new array reference
              ...state.elements, // <-- shallow copy array
              {                  // <-- append new data
                id: (new Date()).toString()
              }
            ],
          };
    
        default:
          return state;
      }
    }
    
    const reducer = (state, action) => {
      switch(action.type) {
        case 'addElement':
          return {
            ...state,                         // <-- shallow copy state,
            elements: state.elements.concat({ // <-- append new data, return new array
              id: (new Date()).toString()
            }),
          };
    
        default:
          return state;
      }
    }
    

    Also, just FYI, using dates as GUIDs is not generally recommended. Better to use a library designed for this. Nano ID is a great library for quickly generating sufficiently unique global ID values.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search