React useReducer Hook触发两次/如何将道具传递给reducer?

时间:2019-03-08 02:17:43

标签: javascript reactjs react-hooks react-context

前言/说明

我正在尝试将React的新挂钩函数用于我正在构建的电子商务网站,但是在解决购物车组件中的错误时遇到了问题。

我认为在讨论之前进行讨论很重要,因为我试图通过使用多个上下文组件来保持全局状态模块化。对于我提供的商品类型,我有一个单独的上下文组件,对于一个人的购物车中的商品,我有一个单独的上下文组件。

问题

我遇到的问题是,当我调度将零件添加到购物车的操作时,减速器将运行两次,就像我两次将零件添加到购物车一样。但是仅当它最初被渲染时,或者由于诸如显示之类的奇怪原因而被设置为hidden,然后又回到blockz-index中的更改以及可能的其他类似更改。

我知道这很冗长,但这是一个很挑剔的问题,因此我创建了两个代码库来展示该问题:

full example

minimum example

您将看到我包含一个用于切换组件display的按钮。这将有助于展示CSS与问题的相关性。

最后,请用代码笔监视控制台,这将显示所有按钮单击以及每个reducer的哪一部分已运行。该问题在full example中最明显,但是控制台语句显示该问题也在minimum example中出现。

问题区域

我已查明问题与以下事实有关:我正在使用useContext挂钩的状态来获取项目列表。调用了一个函数来为我的useReducer钩子生成化简器,但是仅当使用了另一个钩子时才出现,也就是我可以使用像钩子一样不会重新评估的函数,并且没有问题,但我还需要以前的上下文中的信息,以便解决方法不能真正解决我的问题。

相关链接

我已确定问题不是HTML问题,因此我将不包含我尝试过的HTML修复程序的链接。这个问题虽然是由CSS触发的,但并非植根于CSS,所以我也不会包含CSS链接。

useReducer Action dispatched twice

4 个答案:

答案 0 :(得分:9)

正如您所指出的,原因与您链接到的我的related answer相同。每当重新渲染Provider时,您都在重新创建化简器,因此在某些情况下,React将执行化简器以确定是否需要重新渲染Provider以及是否确实需要要重新渲染,它将检测到Reducer已更改,因此React需要执行新的Reducer并使用其产生的新状态,而不是Reducer先前版本返回的状态。

当由于依赖props或上下文或其他状态而不能仅将reducer移出功能组件时,解决方案是使用useCallback来记住您的reducer,以便仅创建一个新的reducer当其依存关系发生变化时(例如您的情况下为productsList)。

要记住的另一件事是,您不必为减速器为一次调度执行两次而过分担心。 React所做的假设是,Reducer通常会足够快(它们无法执行任何副作用,进行API调用等操作),因此值得在某些情况下重新执行它们。为了避免不必要的重新渲染(如果在带有reducer的元素下面有较大的元素层次结构,这可能比reducer昂贵得多)。

这是Provider的修改版本,使用了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>
  );
}

这是您的Codepen的修改版本:https://codepen.io/anon/pen/xBdVMp?editors=0011

以下是与useCallback相关的几个答案,如果您不熟悉如何使用此挂钩,可能会有所帮助:

答案 1 :(得分:2)

将Reducer与帮助我解决矿井的功能部件分开

答案 2 :(得分:0)

基于Ryans出色答案的示例。

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

答案 3 :(得分:0)

当我阅读一些 useContext 源代码时,我发现

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();
    }
  }

第一次更新后,会在更新后调用一个 effect like。在 value 订阅正确的上下文后,例如解析来自 Provider 的值,它请求另一个更新。由于 _ranEffect 标志,这不是循环。

在我看来,如果 React 的上述情况成立,渲染引擎会被调用两次。