【问题标题】:Why is useReducer's dispatch causing re-renders?为什么 useReducer 调度会导致重新渲染?
【发布时间】:2020-05-17 14:43:38
【问题描述】:

假设我像这样实现一个简单的全局加载状态:

// 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);

然后假设我有一个组件会改变加载状态,但从不消耗它,如下所示:

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 />;
};

根据 useReducer 文档,dispatch 具有稳定的身份。我将此解释为,当组件从 useReducer 提取调度时,当连接到该调度的状态发生更改时,它不会重新渲染,因为对调度的引用将始终相同。基本上,dispatch 可以“像静态值一样对待”。

然而,当这段代码运行时,dispatch(actionCreators.setLoadingOn()) 行触发了对全局状态的更新,useLoading 钩子再次运行,dispatch(actionCreators.setLoadingOn()) 也是如此(无限重新渲染-_-)

我没有正确理解 useReducer 吗?还是我正在做的其他事情可能会导致无限重新渲染?

【问题讨论】:

  • doSomethingAsync 可能是问题所在,因为它在每次渲染时都会重新运行。在大多数情况下,您希望用useEffect(() =&gt; {...}, []) 包装doSomethingAsync,以防止它在每次渲染时重新运行。 dispatch(actionCreators.setLoadingOn()); 也是如此。如果它没有包含在 useEffect 中,它将在每次渲染时调度 setLoadingOn,这将导致重新渲染。此伪代码是否正确匹配您的实际问题,或者是否应该对其进行更新以更好地匹配现实与更多useEffects?
  • 您有语法错误。 setLoadingOn(); 不会关闭括号。
  • @Adam 当然是的。该组件主要用于演示目的。实际的 doSomethingAsync 将位于事件处理程序或 useEffect 之类的东西中。
  • @Adam 如果这是一个按钮,也许更现实更现实的例子是。可能是这样的:onClick={() =&gt; dispatch(actionCreators.setLoadingOn())} 抛开细节不谈,在高层次上,我们将拥有一个纯函数式组件,它可以改变某些状态。但是根据钩子的规则,像这样的组件会在每次状态更改时重新渲染,即使它没有订阅它改变的任何状态。当然,我可以使用 useMemo 之类的东西来控制这些组件的重新渲染规则,但仍然如此。看起来很奇怪

标签: javascript reactjs redux react-hooks use-reducer


【解决方案1】:

第一个问题是,你不应该在渲染时触发任何 React 状态更新,包括 useReducersdispatch()useState 的设置器。

第二个问题是,是的,dispatch while 总是导致 React 排队状态更新并尝试调用 reducer,如果 reducer 返回一个新值,React 将继续重新渲染。不管你从哪个组件中分派 - 导致状态更新和重新渲染首先是useReducer 的重点。

“稳定标识”意味着dispatch 变量将指向跨渲染的相同函数引用。

【讨论】:

  • 当然,这更多是出于演示目的。如果这是一个按钮,也许更现实更现实的例子是。可能是这样的: onClick={() => dispatch(actionCreators.setLoadingOn())} 抛开细节不谈,在高层次上,我们将拥有一个纯函数式组件,它可以改变某些状态。但是根据钩子的规则,像这样的组件会在每次状态更改时重新渲染,即使它没有订阅它改变的任何状态。当然,我可以使用 useMemo 之类的东西来控制这些组件的重新渲染规则,但仍然如此。看起来很奇怪
  • 不确定您在这里指的是什么问题。请记住,React 的默认行为是总是在每次状态更改时递归地重新渲染。在这方面,useReducer 没有什么特别之处——它只是在组件中组织状态更新逻辑的不同机制。
  • 是的,我现在看到了。谢谢。现在我很好奇useReduceruseState 有什么优势。当我只使用useStateContext 实现全局状态时,我会通过全局状态挂钩传递getter 和setter 回调。我认为这对于只使用 setter 回调的组件来说是不好的做法,因为它会导致不必要的重新渲染。我以为“身份安全”派送会解决这个问题,但它没有哈哈。
  • dispatch 和 setter 的稳定标识是有用的)。总体而言,useReducer 如果您有复杂的状态更新逻辑,需要避免必须读取当前状态以计算新状态的闭包,或者希望允许子组件仅指示“某些事件发生”并保持逻辑更高,则很有帮助起来。
  • “你应该永远在渲染时触发任何 React 状态更新” - React 文档mention some (rare) cases,在渲染中更新状态是有效的。 (=getDerivedStateFromProps)。尽管我可以想象,这种模式在并发模式下会出现问题。
【解决方案2】:

除了您在渲染时设置状态这一事实已经指出,我想我可以阐明如何利用调度的稳定身份来避免像您期望的那样不必要的重新渲染。

您的 Provider 值是一个对象 (value={{ isLoading, dispatch}})。这意味着当上下文的状态改变时(例如,当 isLoading 改变时),值本身的标识也会改变。因此,即使您有一个组件像这样使用调度:

const { dispatch } = useLoading()

当 isLoading 改变时组件会重新渲染。

如果您觉得重新渲染已经失控,利用调度稳定身份的方法是创建两个提供程序,一个用于状态(在本例中为 isLoading),一个用于分派,如果你这样做,一个只需要像这样分派的组件:

const dispatch = useLoadingDispatch()

不会在 isLoading 更改时重新渲染。

请注意,这可能是过度优化,在简单的场景中可能不值得。

这是一组优秀的文章,供您进一步阅读该主题: https://kentcdodds.com/blog/how-to-optimize-your-context-value https://kentcdodds.com/blog/how-to-use-react-context-effectively

【讨论】:

    猜你喜欢
    • 2021-05-11
    • 1970-01-01
    • 1970-01-01
    • 2020-09-08
    • 2021-01-17
    • 1970-01-01
    • 1970-01-01
    • 2020-10-20
    • 1970-01-01
    相关资源
    最近更新 更多