React useReducer Hook 触发两次/如何将 props 传递给 reducer?
React useReducer Hook fires twice / how to pass props to reducer?
前言/描述
我正在尝试将 React 的新挂钩功能用于我正在构建的电子商务网站,但在解决我的购物车组件中的错误时遇到了问题。
我认为在讨论前先说明我试图通过使用多个 Context 组件来保持全局状态模块化这一事实是相关的。我有一个单独的上下文组件用于我提供的项目类型,还有一个单独的上下文组件用于一个人的购物车中的项目。
问题
我遇到的问题是,当我发送一个将组件添加到我的购物车的操作时,reducer 将 运行 两次,就好像我将商品添加到我的购物车两次一样。但仅当它最初被渲染时,或出于奇怪的原因,例如显示设置为 hidden
然后返回 block
或更改 z-index
以及其他可能的类似更改。
我知道这有点冗长,但这是一个相当挑剔的问题,所以我创建了两个代码笔来展示这个问题:
您会看到我包含了一个按钮来切换组件的 display
。这将有助于展示 css 与问题的相关性。
最后请在代码笔中监控控制台,这将显示所有按钮点击以及每个 reducer 的哪一部分被点击 运行。这些问题在 full example 中最为明显,但控制台语句显示的问题也存在于 minimum example.
中
问题领域
我已经确定问题与我正在使用 useContext
挂钩的状态来获取项目列表这一事实有关。调用一个函数来为我的 useReducer
钩子生成 reducer,但只有在使用不同的钩子时才会出现,也就是我可以使用一个不会像钩子那样重新评估的函数并且没有问题,但我还需要我以前的上下文中的信息,这样解决方法并不能真正解决我的问题。
相关链接
我已确定该问题不是 HTML 问题,因此我不会包含指向我尝试过的 HTML 修复的链接。该问题虽然由 css 触发,但并非根源于 css,因此我也不会包含 css 链接。
正如您所指出的,原因与您链接到的我的 相同。每当 Provider
重新渲染时,你都会重新创建你的 reducer,所以在某些情况下,React 会执行 reducer 以确定它是否需要重新渲染 Provider
以及是否需要重新渲染它会检测到 reducer 已更改,因此 React 需要执行新的 reducer 并使用它产生的新状态,而不是以前版本的 reducer 返回的状态。
当由于依赖于 props 或上下文或其他状态而不能将 reducer 移出功能组件时,解决方案是使用 useCallback
记住你的 reducer,这样你只需要创建一个当它的依赖关系发生变化时新的减速器(例如 productsList
在你的情况下)。
另一件要记住的事情是,你不应该太担心你的 reducer 为一次调度执行两次。 React 所做的假设是 reducers 通常会足够快(它们不能做任何有副作用的事情,进行 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>
);
}
这是您的代码笔的修改版本:https://codepen.io/anon/pen/xBdVMp?editors=0011
这里有几个与 useCallback
相关的答案,如果您不熟悉如何使用此挂钩,可能会对您有所帮助:
将 Reducer 与帮助我解决问题的功能组件分开
基于 Ryan 出色回答的示例。
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);
当我阅读一些 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
成立,渲染引擎将被调用两次。
前言/描述
我正在尝试将 React 的新挂钩功能用于我正在构建的电子商务网站,但在解决我的购物车组件中的错误时遇到了问题。
我认为在讨论前先说明我试图通过使用多个 Context 组件来保持全局状态模块化这一事实是相关的。我有一个单独的上下文组件用于我提供的项目类型,还有一个单独的上下文组件用于一个人的购物车中的项目。
问题
我遇到的问题是,当我发送一个将组件添加到我的购物车的操作时,reducer 将 运行 两次,就好像我将商品添加到我的购物车两次一样。但仅当它最初被渲染时,或出于奇怪的原因,例如显示设置为 hidden
然后返回 block
或更改 z-index
以及其他可能的类似更改。
我知道这有点冗长,但这是一个相当挑剔的问题,所以我创建了两个代码笔来展示这个问题:
您会看到我包含了一个按钮来切换组件的 display
。这将有助于展示 css 与问题的相关性。
最后请在代码笔中监控控制台,这将显示所有按钮点击以及每个 reducer 的哪一部分被点击 运行。这些问题在 full example 中最为明显,但控制台语句显示的问题也存在于 minimum example.
中问题领域
我已经确定问题与我正在使用 useContext
挂钩的状态来获取项目列表这一事实有关。调用一个函数来为我的 useReducer
钩子生成 reducer,但只有在使用不同的钩子时才会出现,也就是我可以使用一个不会像钩子那样重新评估的函数并且没有问题,但我还需要我以前的上下文中的信息,这样解决方法并不能真正解决我的问题。
相关链接
我已确定该问题不是 HTML 问题,因此我不会包含指向我尝试过的 HTML 修复的链接。该问题虽然由 css 触发,但并非根源于 css,因此我也不会包含 css 链接。
正如您所指出的,原因与您链接到的我的 Provider
重新渲染时,你都会重新创建你的 reducer,所以在某些情况下,React 会执行 reducer 以确定它是否需要重新渲染 Provider
以及是否需要重新渲染它会检测到 reducer 已更改,因此 React 需要执行新的 reducer 并使用它产生的新状态,而不是以前版本的 reducer 返回的状态。
当由于依赖于 props 或上下文或其他状态而不能将 reducer 移出功能组件时,解决方案是使用 useCallback
记住你的 reducer,这样你只需要创建一个当它的依赖关系发生变化时新的减速器(例如 productsList
在你的情况下)。
另一件要记住的事情是,你不应该太担心你的 reducer 为一次调度执行两次。 React 所做的假设是 reducers 通常会足够快(它们不能做任何有副作用的事情,进行 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>
);
}
这是您的代码笔的修改版本:https://codepen.io/anon/pen/xBdVMp?editors=0011
这里有几个与 useCallback
相关的答案,如果您不熟悉如何使用此挂钩,可能会对您有所帮助:
将 Reducer 与帮助我解决问题的功能组件分开
基于 Ryan 出色回答的示例。
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);
当我阅读一些 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
成立,渲染引擎将被调用两次。