React useReducer Hook fires twice / how to pass props to reducer?
Asked Answered
M

5

21

FOREWORD / DESCRIPTION

I am trying to use React's new hooks feature for an e-commerce website that I am building, and have been having an issue working a bug out of my shopping cart component.

I think it is relevant to preface the discussion with the fact that I am trying to keep my global state modular by using multiple Context components. I have a separate context component for the types of items that I offer, and a separate context component for the items in a person's shopping cart.

PROBLEM

The issue I am having is that when I dispatch an action to add a component to my cart, the reducer will run twice as if I had added the item to my cart twice. But only when it is initially rendered, or for weird reasons such as the display is set to hidden and then back to block or for a change in the z-index and potentially other similar changes.

I know this is kind of verbose, but it is rather knit picky issue so I have created two codepens that showcase the issue:

full example

minimum example

You will see that I have included a button to toggle the display of the components. This will help showcase the correlation of the css to the issue.

Finally please monitor the console in the code pens, this will show all button clicks and which part of each reducer has been run. The issues are most evident in the full example, but the console statements display the issue is also present in the minimum example.

PROBLEM AREA

I have pinpointed the problem to be related to the fact that I am using the state of a useContext hook to get the items list. A function is called to generate the reducer for my useReducer hook, but only arises when a different hook is used AKA I could use a function that wouldn't be subject to re-eval like hook is and not have the issue, but I also need the info from my previous Context so that workaround doesn't really fix my issue.

Relevant Links

I have determined the issue is NOT an HTML issue so I will not include the links to the HTML fixes I have tried. The issue, while triggered by css, is not rooted in css so I will not include css links either.

useReducer Action dispatched twice

Miguelmiguela answered 8/3, 2019 at 2:17 Comment(0)
J
43

As you indicated, the cause is the same as the related answer of mine that you linked to. You are re-creating your reducer whenever Provider is re-rendered, so in some cases React will execute the reducer in order to determine whether or not it needs to re-render Provider and if it does need to re-render it will detect that the reducer is changed, so React needs to execute the new reducer and use the new state produced by it rather than what was returned by the previous version of the reducer.

When you can't just move the reducer out of your function component due to dependencies on props or context or other state, the solution is to memoize your reducer using useCallback, so that you only create a new reducer when its dependencies change (e.g. productsList in your case).

The other thing to keep in mind is that you shouldn't worry too much about your reducer executing twice for a single dispatch. The assumption React is making is that reducers are generally going to be fast enough (they can't do anything with side effects, make API calls, etc.) that it is worth the risk of needing to re-execute them in certain scenarios in order to try to avoid unnecessary re-renders (which could be much more expensive than the reducer if there is a large element hierarchy underneath the element with the reducer).

Here's a modified version of Provider using useCallback:

const Context = React.createContext();
const Provider = props => {
  const memoizedReducer = React.useCallback(createReducer(productsList), [productsList])
  const [state, dispatch] = React.useReducer(memoizedReducer, []);

  return (
    <Context.Provider value={{ state, dispatch }}>
      {props.children}
    </Context.Provider>
  );
}

Here is a modified version of your codepen: https://codepen.io/anon/pen/xBdVMp?editors=0011

Here are a couple answers related to useCallback that might be helpful if you aren't familiar with how to use this hook:

Joellenjoelly answered 8/3, 2019 at 4:13 Comment(3)
Wow this answer is golden, much appreciated!Miguelmiguela
@Ryan couldn't yet take time to fully read this and other answer of yours, but AFAIK, this shouldn't cause bugs right? (if your reducer is pure, and no side effects). Because if reducer is called twice for each increment action say, the second call still gets state as the first call. You may want to better highlight this, you do hint on it though.Stepparent
@gmoniava Correct. The reducer being called twice should be harmless.Joellenjoelly
S
1

Seperate the Reducer from the functional component that helped me solve mine

Siena answered 16/1, 2020 at 16:33 Comment(1)
'seperate the reducer' what does that mean? I get an error ' React Hook "useReducer" cannot be called at the top level.' when trying to separate the reducer?Gallonage
H
0

An example based on Ryans excellent answer.

  const memoizedReducer = React.useCallback((state, action) => {
    switch (action.type) {
      case "addRow":
        return [...state, 1];
      case "deleteRow":
        return [];
      default:
        throw new Error();
    }
  }, []) // <--- if you have vars/deps inside the reducer that changes, they need to go here

  const [data, dispatch] = React.useReducer(memoizedReducer, _data);

Hoick answered 17/6, 2020 at 6:33 Comment(0)
B
0

When I read some useContext source code, i found

const useContext = hook(class extends Hook {
  call() {
    if(!this._ranEffect) {
      this._ranEffect = true;
      if(this._unsubscribe) this._unsubscribe();
      this._subscribe(this.Context);
      this.el.update();
    }
  }

After the first time update, a effect like is called after the update. After the value is subscribed to the right context, for instance, resolving the value from Provider, it requests another update. This is not a loop, thanks to _ranEffect flag.

Seems to me if above is true for React, the render engine are called twice.

Bryner answered 12/5, 2021 at 21:48 Comment(0)
D
0

Do you use StrictMode in your app ?

https://beta.reactjs.org/reference/react/StrictMode

According to the documentation:

" Strict Mode enables extra development-only checks for the entire component tree inside the component. These checks help you find common bugs in your components early in the development process. "

" Although the Strict Mode checks only run in development, they help you find bugs that already exist in your code but can be tricky to reliably reproduce in production. Strict Mode lets you fix bugs before your users report them.

" Strict Mode enables the following checks in development:

Your components will re-render an extra time to find bugs caused by impure rendering.

Your components will re-run Effects an extra time to find bugs caused by missing Effect cleanup.

Your components will be checked for usage of deprecated APIs.

All of these checks are development-only and do not impact the production build. "

"React assumes that every component you write is a pure function. This means that React components you write must always return the same JSX given the same inputs (props, state, and context).

Components breaking this rule behave unpredictably and cause bugs. To help you find accidentally impure code, Strict Mode calls some of your functions (only the ones that should be pure) twice in development. This includes:

  • Your component function body (only top-level logic, so this doesn’t include code inside event handlers)

  • Functions that you pass to useState, set functions, useMemo, or useReducer Some class component methods like constructor, render, shouldComponentUpdate (see the whole list)

  • If a function is pure, running it twice does not change its behavior because a pure function produces the same result every time."

Hope this help you !

Dolly answered 8/3, 2023 at 9:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.