Search code examples
reactjsuse-stateuse-reducer

Using setState from inside a reducer. Reducer updates state twice instead of once


I am new to React and I'm trying to build a calculator in React.js. I have a display that contains two fields: one to output the current value and the other one to store a "slice" of the expression. I'm basically trying to immitate the Windows calculator. I use useReducer hook to update state depending on the type of the button pressed. In the dispatch object, I send references to two useState hooks declared inside the App. These hooks are: (1) a useState boolean to indicate whether I am waiting for a second amount, and (2) a useState to save a slice of an expression so it appears in the paragraph above.

The problem is in the reducer function, in the "multiplication_operator" case. When I click on the multiplier sign for the second time, I expect the previous state array ['first amount', 'operator'] to concatenate with the 'second amount'. Basically, I expect an array where index 0 is the first amount, index 1 is the operator and index 2 is the second amount. However, when I try to add the second amount at index 2, it adds the same second amount at the next index (3), which is unexpected. Link to Codesandbox

Link to gif that reproduces the bug

I assume the problem might come from the fact that I use and update too many states from inside the reducer function.

My question is what is the source of the problem and how do I fix it? Is it the issue of poor choice of hooks or something else? Reducer triggers a chain of updates from App and then back to reducer? I'm really out of ideas here.


Solution

  • It's actually the correct behaviour for useReducer to be called twice. (Details here: https://github.com/facebook/react/issues/16295)

    For your app to work as intended despite this, you would have to store/update value, firstNumber and operatorIsActive within the reducer instead of calling setFirstNumber and setOperatorIsActive to update them.

    e.g. you could set the initial state of your reducer as

    const initialState = {
      value: "0",
      firstNumber: [],
      operatorIsActive: false
    };
    

    instead of just storing the initial value.

    const initialValue = "0";
    

    In App.js you can access the reducer values this way

    const [state, dispatch] = useReducer(displayValueReducer, initialState);
    const {value, firstNumber, operatorIsActive} = state;
    

    When the multiply button is clicked, you could access/update the values this way:

    case "multiplication_operator": {
      const operator = ` ${action.value} `;
      if (state.operatorIsActive === false) {
        return {
          ...state,
          firstNumber: [state.value, operator],
          operatorIsActive: true
        };
      } else {
        return {
          ...state,
          firstNumber: [...state.firstNumber, state.value]
        };
      }
    }
    

    Changes like the above would ensure that your reducer is pure (elaboration here: https://www.geeksforgeeks.org/pure-functions-in-javascript/)