Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is useReducer's dispatch causing re-renders?

Suppose I implement a simple global loading state like this:

// hooks/useLoading.js
import React, { createContext, useContext, useReducer } from 'react';

const Context = createContext();

const { Provider } = Context;

const initialState = {
  isLoading: false,
};

function reducer(state, action) {
  switch (action.type) {
    case 'SET_LOADING_ON': {
      return {
        ...state,
        isLoading: true,
      };
    }
    case 'SET_LOADING_OFF': {
      return {
        ...state,
        isLoading: false,
      };
    }
  }
}

export const actionCreators = {
  setLoadingOn: () => ({
    type: 'SET_LOADING_ON',
  }),
  setLoadingOff: () => ({
    type: 'SET_LOADING_OFF',
  }),
};

export const LoadingProvider = ({ children }) => {
  const [{ isLoading }, dispatch] = useReducer(reducer, initialState);
  return <Provider value={{ isLoading, dispatch }}>{children}</Provider>;
};

export default () => useContext(Context);

Then suppose I have a component that mutates the loading state, but never consumes it, like this:

import React from 'react';
import useLoading, { actionCreators } from 'hooks/useLoading';

export default () => {
  const { dispatch } = useLoading();
  dispatch(actionCreators.setLoadingOn();
  doSomethingAsync().then(() => dispatch(actionCreators.setLoadingOff()))
  return <React.Fragment />;
};

According to useReducer docs, dispatch is has a stable identity. I interpreted this to mean that when a component extracts dispatch from a useReducer, it won't re-render when the state connected to that dispatch changes, because the reference to dispatch will always be the same. Basically, dispatch can "treated like a static value".

Yet when this code runs, the line dispatch(actionCreators.setLoadingOn()) triggers an update to global state and the useLoading hook is ran again and so is dispatch(actionCreators.setLoadingOn()) (infinite re-renders -_-)

Am I not understanding useReducer correctly? Or is there something else I'm doing that might be causing the infinite re-renders?

like image 445
adrayv Avatar asked Jan 31 '20 22:01

adrayv


2 Answers

The first issue is that you should never trigger any React state updates while rendering, including useReducers's dispatch() and useState's setters.

The second issue is that yes, dispatching while always cause React to queue a state update and try calling the reducer, and if the reducer returns a new value, React will continue re-rendering. Doesn't matter what component you've dispatched from - causing state updates and re-rendering is the point of useReducer in the first place.

The "stable identity" means that the dispatch variable will point to the same function reference across renders.

like image 138
markerikson Avatar answered Sep 21 '22 06:09

markerikson


Besides the fact that you're setting state while rendering as has been pointed out, I think I can shed some light about how to take advantage dispatch's stable identity to avoid unnecessary re-renders like you are expecting.

Your Provider value is an object (value={{ isLoading, dispatch}}). This means the identity of the value itself will change when the context's state changes (for example, when isLoading changes). So even if you have a component where you only consume dispatch like so:

const { dispatch } = useLoading()

The component will re-render when isLoading changes.

If you're at the point where you feel re-rendering is getting out of hand, the way to take advantage of dispatch stable identity is to create two Providers, one for the state (isLoading in this case) and one for dispatch, if you do this, a component that only needs dispatch like so:

const dispatch = useLoadingDispatch()

Will not re-render when isLoading changes.

Note that this can be an overoptimization and in simple scenarios might not be worth it.

This is an excellent set of articles for further reading on the subject: https://kentcdodds.com/blog/how-to-optimize-your-context-value https://kentcdodds.com/blog/how-to-use-react-context-effectively

like image 33
G Gallegos Avatar answered Sep 21 '22 06:09

G Gallegos