useReducer Action dispatched twice
Asked Answered
F

7

44

Scenario

I have a custom hook that returns an action. The parent component "Container" utilized the custom hook and pass the action as prop to a children component.

Problem

When the action is executed from a child component, the actual dispatch occurs twice. Now, if the children utilize the hook directly and invoked the action, the dispatch occurs only once.

How to reproduce it:

Open the below sandbox and open devtools on chrome so you can see the console logs I've added.

https://codesandbox.io/s/j299ww3lo5?fontsize=14

Main.js (children component) you will see we invoke props.actions.getData()

On the DevTools, clear the Logs. On the Preview, enter any value on the form and click the button. On the console log, you will see the actions like redux-logger and you will notice STATUS_FETCHING action is executed twice without changing the state.

Now go to Main.js and comment out line 9 and uncomment line 10. We are now basically consuming the custom hook directly.

On the DevTools, clear the Logs. On the Preview, enter any value on the form and click the button. On the console log, now you will see STATUS_FETCHING has executed only once and the state changes accordingly.

While there is no evident perf penalty as it is, I fail to understand WHY is it happening. I may be too focused on the Hooks and I'm missing something so silly...please release me from this puzzle. Thanks!

Firry answered 26/2, 2019 at 19:3 Comment(1)
Seems, the linked question codesandbox itself has been fixed by OP. Ryan Cogswell's answer has the original code.Iloilo
S
61

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

Spiritless answered 26/2, 2019 at 21:50 Comment(2)
@RyanCogswell Great. That makes perfect sense. One last bit though: The React source code snippet you highlighted earlier on in your answer seems to compare reducer states... however in this scenario React also needs to compare the reducer itself... any clues where this is happening in the source code?Needy
@kstratis The reducer is compared here: github.com/facebook/react/blob/v16.9.0/packages/…Spiritless
O
33

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

Officialese answered 14/4, 2020 at 13:14 Comment(2)
Wow, this is so incredible... it was exactly that, StrictMode was causing the reducer to be called twice... But I thought that was "impossible" or some other issue since console.log calls in the reducer were not showing up doubled.... until I read this reactjs.org/docs/… "Starting with React 17, React automatically modifies the console methods like console.log() to silence the logs in the second call to lifecycle functions" (!!!!) see also thisSubaxillary
Warning: Strict mode is revealing an issue that may arise in concurrent mode. Strict mode is revealing the issue to you!!Unvoice
R
25

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.

Roil answered 5/7, 2020 at 4:52 Comment(0)
B
13

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.

Babbie answered 18/9, 2020 at 6:44 Comment(2)
I am having this problem, but this answer doesn't seem to make sense to me. The state is updated, and so the input to the reducer has in fact changed. The state is not the same on the second entry. Say, for example, you have a toggle; running it twice will revert it to its original state...are we saying that reducers should not use parts of the old state to generate new state?Stuck
@Stuck it's ok, you can use the previous state, which the reducer receives as its first argument, to build next state. The state and action passed to the reducer would be the same both times; it is not passing the result of the first call to the second call, just duplicate identical calls. The thing is if you are manipulating outside data (e.g. closure) within the reducer function, then that's a side effect and will happen every reducer call. So move the side effect out into a useEffect, and just change state (increment a number maybe), and have the useEffect depend on that state change.Downcomer
F
8

by removing <React.StrictMode> dispatch won't be called several times, I was facing this issue as well and this solution worked

Fca answered 12/7, 2020 at 12:17 Comment(2)
I removed <React.StrictMode>, but im getting same problem.Marvellamarvellous
This has been mentioned a few times in other answers. Consider upvoting those instead of adding a "worked for me" answer. If all 50-odd people did this instead of upvoting, the thread would be a mess.Opportunity
P
0

Other answers mention that React.StrictMode cause the dispatch function to be run twice. Doing so should result in the same result both times, because reducers are supposed to be deterministic. If the effect of the dispatch is applied twice and you're using an object to represent your state, it might be because you're making a shallow copy of the state variable. If you make a shallow copy, changing the attributes will change the original state variable, and thus produce a side-effect. To make a deep copy try:

const new_state = JSON.parse(JSON.stringify(state))

P.S. If this is as gross to you as it is to me, keep in mind this is the method suggested by Mozilla. I don't know of a better way.

P.P.S This would have been a comment if I had enough karma.

Platitudinous answered 3/3, 2023 at 0:57 Comment(1)
I would suggest to use structuredClone() instead of JSON methods, but it might work only on the newest browser.Zeralda
M
-3

This works for me. by removing <React.StrictMode> dispatch won't be called several times, I was facing this issue as well and this solution worked

Mete answered 10/7, 2022 at 5:48 Comment(3)
This will probably have other side effects which aren't desired, no?Dola
This has been mentioned a few times in other answers. Consider upvoting those instead of adding a "worked for me" answer. If all 50-odd people did this instead of upvoting, the thread would be a mess.Opportunity
You've literally quoted the solution and added "This works for me". It's much more useful to upvote the solution so that it floats to the top. This removes clutter from the answers also.Unyoke

© 2022 - 2024 — McMap. All rights reserved.