Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

useReducer Action dispatched twice

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!

like image 549
sesteva Avatar asked Feb 26 '19 19:02

sesteva


People also ask

What does useReducer Dispatch Return?

1. useReducer() The useReducer(reducer, initialState) hook accept 2 arguments: the reducer function and the initial state. The hook then returns an array of 2 items: the current state and the dispatch function.

Why a dispatch to a reducer causes all reducers get called?

This is because the suggested Redux reducer structure is "reducer composition", where many mostly-independent reducer functions can be combined into one structure, and many reducer functions could potentially respond to a single action and update their own slice of state.

Is useReducer better than Redux?

Can useReducer replace Redux? The useReducer hook should be used in components that have complex logic behind it. It shows as the main confusion with the Redux library, because developers tend to think that useReducer could replace the state manager library. But in fact, its use should be restricted to components.

Can I use useEffect twice?

If you have just made a new project using Create React App or updated to React version 18, you will notice that the useEffect hook is called twice in development mode. This is the case whether you used Create React App or upgraded to React version 18.


1 Answers

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

like image 158
Ryan Cogswell Avatar answered Sep 18 '22 13:09

Ryan Cogswell