Codehardt
Codehardt

Reputation: 43

React's reducer returns unexpected state that is not consistent with logged value

I created a reducer that increments a global UID on every dispatch with the action increment. The initial value of the UID is 0.

Expectation:

I expected that the UID is incremented by 1 on every dispatch.

Reality:

The UID increments by 2 and the logged state are inconsistent with the real state's value.

My component:

import React, { useReducer }  from "react"

let uid = 0

function nextUID() {
    uid = uid + 1
    return uid
}

function reducer(state, action) {
    switch (action.type) {
        case "increment":
            const uid = nextUID()
            const newState = `current UID is ${uid}!`
            console.log(newState)
            return newState
        default:
            return state
    }
}

function TestComponent() {
    const [state, dispatch] = useReducer(reducer, "not clicked yet")
    return <button onClick={() => dispatch({ type: "increment" })}>{state}</button>
}

export default TestComponent

Output:

Num. Clicks Button Label Console Output
0 not yet clicked
1 current UID is 1! current UID is 1!
2 current UID is 3! current UID is 2!
3 current UID is 5! current UID is 4!

Question:

How is it possible, that the button label is current UID is 3! while the console output for the state change was current UID is 2!? Is it possible that react calls the reducer multiple times and discards the console output the second time?

Additional Information:

Thanks.

Upvotes: 3

Views: 199

Answers (1)

Lakshya Thakur
Lakshya Thakur

Reputation: 8316

Alright StrictMode is messing with you but in a good way. The code that you have written is undesirable as per React. StrictMode calls your reducer two times to eliminate any side-effects. In your case, the implementation of incrementing uid is one such side-effect.

(Simply doing console.log wouldn't show you that reducer is being called twice since React silences it for second call from 17.0. So I have used a log function to refer to the same console.log);

Here is your code :-

import React, { useReducer } from "react";

const log = console.log;

let uid = 0;

function nextUID() {
  uid = uid + 1;
  return uid;
}

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      log("called reducer");
      const uid = nextUID();
      const newState = `current UID is ${uid}!`;
      console.log(newState);
      return newState;
    default:
      return state;
  }
}

function TestComponent() {
  const [state, dispatch] = useReducer(reducer, "not clicked yet");
  return (
    <button onClick={() => dispatch({ type: "increment" })}>{state}</button>
  );
}

export default TestComponent;

Edit React useReducer twice in strict mode

Now your original code would work fine in production because the reducer would be called only once (since productions doesn't have Strict Mode wrapper component) but then it's not a good practice.

Following is another implementation where state is not mutated directly and so between two calls to the reducer previous state remains the same and the next increment value is consistent. If you mutated state directly like state.val += 1, you would see the same behaviour as your example in Strict mode.

import React, { useReducer } from "react";

const log = console.log;

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      log("called reducer");
      const nextStateVal = state.val + 1;
      return {
        ...state,
        text: `current UID is ${nextStateVal}`,
        val: nextStateVal
      };
    default:
      return state;
  }
}

function TestComponent() {
  const [state, dispatch] = useReducer(reducer, {
    text: "not clicked yet",
    val: 0
  });
  return (
    <button onClick={() => dispatch({ type: "increment" })}>
      {state.text}
    </button>
  );
}

export default TestComponent;

Edit React useReducer strict mode correct way

Upvotes: 3

Related Questions