useReducer Action dispatched twice

Remove the <React.StrictMode> will be fixed the issues.


As React docs says:

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: [...] Functions passed to useState, useMemo, or useReducer

This is because reducers must be pure, they must give the same output with the same arguments every time, and React Strict Mode tests (sometimes) this automatically by calling your reducer twice.

This shoudn't be a problem because is a behaviour limited to development, it won't appear in production, so I wouldn't recommend quitting <React.StrictMode> because it can be very helpfull in highlighting many problems related with keys, sideEffects, and many more.


To first clarify the existing behavior, the STATUS_FETCHING action was actually only being "dispatched" (i.e. if you do a console.log right before the dispatch call in getData within useApiCall.js) once, but the reducer code was executing twice.

I probably wouldn't have known what to look for to explain why if it hadn't been for my research when writing this somewhat-related answer: React hook rendering an extra time.

You'll find the following block of code from React shown in that answer:

  var currentState = queue.eagerState;
  var _eagerState = _eagerReducer(currentState, action);
  // Stash the eagerly computed state, and the reducer used to compute
  // it, on the update object. If the reducer hasn't changed by the
  // time we enter the render phase, then the eager state can be used
  // without calling the reducer again.
  _update2.eagerReducer = _eagerReducer;
  _update2.eagerState = _eagerState;
  if (is(_eagerState, currentState)) {
    // Fast path. We can bail out without scheduling React to re-render.
    // It's still possible that we'll need to rebase this update later,
    // if the component re-renders for a different reason and by that
    // time the reducer has changed.
    return;
  }

In particular, notice the comments indicating React may have to redo some of the work if the reducer has changed. The issue is that in your useApiCallReducer.js you were defining your reducer inside of your useApiCallReducer custom hook. This means that on a re-render, you provide a new reducer function each time even though the reducer code is identical. Unless your reducer needs to use arguments passed to the custom hook (rather than just using the state and action arguments passed to the reducer), you should define the reducer at the outer level (i.e. not nested inside another function). In general, I would recommend avoiding defining a function nested within another unless it actually uses variables from the scope it is nested within.

When React sees the new reducer after the re-render, it has to throw out some of the work it did earlier when trying to determine whether a re-render would be necessary because your new reducer might produce a different result. This is all just part of performance optimization details in the React code that you mostly don't need to worry about, but it is worth being aware that if you redefine functions unnecessarily, you may end up defeating some performance optimizations.

To solve this I changed the following:

import { useReducer } from "react";
import types from "./types";

const initialState = {
  data: [],
  error: [],
  status: types.STATUS_IDLE
};

export function useApiCallReducer() {
  function reducer(state, action) {
    console.log("prevState: ", state);
    console.log("action: ", action);
    switch (action.type) {
      case types.STATUS_FETCHING:
        return {
          ...state,
          status: types.STATUS_FETCHING
        };
      case types.STATUS_FETCH_SUCCESS:
        return {
          ...state,
          error: [],
          data: action.data,
          status: types.STATUS_FETCH_SUCCESS
        };
      case types.STATUS_FETCH_FAILURE:
        return {
          ...state,
          error: action.error,
          status: types.STATUS_FETCH_FAILURE
        };
      default:
        return state;
    }
  }
  return useReducer(reducer, initialState);
}

to instead be:

import { useReducer } from "react";
import types from "./types";

const initialState = {
  data: [],
  error: [],
  status: types.STATUS_IDLE
};
function reducer(state, action) {
  console.log("prevState: ", state);
  console.log("action: ", action);
  switch (action.type) {
    case types.STATUS_FETCHING:
      return {
        ...state,
        status: types.STATUS_FETCHING
      };
    case types.STATUS_FETCH_SUCCESS:
      return {
        ...state,
        error: [],
        data: action.data,
        status: types.STATUS_FETCH_SUCCESS
      };
    case types.STATUS_FETCH_FAILURE:
      return {
        ...state,
        error: action.error,
        status: types.STATUS_FETCH_FAILURE
      };
    default:
      return state;
  }
}

export function useApiCallReducer() {
  return useReducer(reducer, initialState);
}

Edit useAirportsData

Here's a related answer for a variation on this problem when the reducer has dependencies (e.g. on props or other state) that require it to be defined within another function: React useReducer Hook fires twice / how to pass props to reducer?

Below is a very contrived example to demonstrate a scenario where a change in the reducer during render requires it to be re-executed. You can see in the console, that the first time you trigger the reducer via one of the buttons, it executes twice -- once with the initial reducer (addSubtractReducer) and then again with the different reducer (multiplyDivideReducer). Subsequent dispatches seem to trigger the re-render unconditionally without first executing the reducer, so only the correct reducer is executed. You can see particularly interesting behavior in the logs if you first dispatch the "nochange" action.

import React from "react";
import ReactDOM from "react-dom";

const addSubtractReducer = (state, { type }) => {
  let newState = state;
  switch (type) {
    case "increase":
      newState = state + 10;
      break;
    case "decrease":
      newState = state - 10;
      break;
    default:
      newState = state;
  }
  console.log("add/subtract", type, newState);
  return newState;
};
const multiplyDivideReducer = (state, { type }) => {
  let newState = state;
  switch (type) {
    case "increase":
      newState = state * 10;
      break;
    case "decrease":
      newState = state / 10;
      break;
    default:
      newState = state;
  }
  console.log("multiply/divide", type, newState);
  return newState;
};
function App() {
  const reducerIndexRef = React.useRef(0);
  React.useEffect(() => {
    reducerIndexRef.current += 1;
  });
  const reducer =
    reducerIndexRef.current % 2 === 0
      ? addSubtractReducer
      : multiplyDivideReducer;
  const [reducerValue, dispatch] = React.useReducer(reducer, 10);
  return (
    <div>
      Reducer Value: {reducerValue}
      <div>
        <button onClick={() => dispatch({ type: "increase" })}>Increase</button>
        <button onClick={() => dispatch({ type: "decrease" })}>Decrease</button>
        <button onClick={() => dispatch({ type: "nochange" })}>
          Dispatch With No Change
        </button>
      </div>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Edit different reducers


If you're using React.StrictMode, React will call your reducer multiple times with the same arguments to test the purity of your reducer. You can disable StrictMode, to test if your reducer is correctly memoized.

From https://github.com/facebook/react/issues/16295#issuecomment-610098654:

There is no "problem". React intentionally calls your reducer twice to make any unexpected side effects more apparent. Since your reducer is pure, calling it twice doesn't affect the logic of your application. So you shouldn't worry about this.

In production, it will only be called once.