避免在 Context 更新时运行效果挂钩 [英] Avoid runnning an effect hook when Context get updated
问题描述
我有一个组件 MyContainer
,它有一个状态变量(通过 useState
钩子定义),定义了一个上下文提供程序,它将状态变量作为值传递给它,并且还包含2 个孩子,MySetCtxComponent
和 MyViewCtxComponent
.
MySetCtxComponent
可以更改存储在上下文中的值,调用 set 函数,该函数也作为上下文的一部分传递,但不渲染它.
MyViewCtxComponent
,相反,RENDERS 存储在上下文中的值.
MySetCtxComponent
还通过 useEffect
钩子定义了一个效果.例如,这种效果用于以固定的时间间隔更新上下文的值.
所以3个组件的代码是这样的
我的容器
导出函数 MyContainer() {const [myContextValue, setMyContextValue] = useState(null);const setCtxVal = (newVal: string) =>{setMyContextValue(newVal);};返回 (<MyContext.Providervalue={{ value: myContextValue, setMyContextValue: setCtxVal }}><MySetCtxComponent/><MyViewCtxComponent/></MyContext.Provider>);}
MySetCtxComponent(加上一个全局变量使示例更简单)
let counter = 0;导出函数 MySetCtxComponent() {const myCtx = useContext(MyContext);useEffect(() => {console.log("========>>>>>>>>>>> 在 MySetCtxComponent 中使用效果运行");const intervalID = setInterval(() => {myCtx.setMyContextValue("新值" + counter);计数器++;}, 3000);返回 () =>清除间隔(间隔ID);}, [myCtx]);return
MyViewCtxComponent
导出函数 MyViewCtxComponent() {const myCtx = useContext(MyContext);返回 (<div>这是上下文的值:{myCtx.value}
);}
现在我的问题是,这样,每次更新上下文时,MySetCtxComponent
的效果都会再次运行,即使这根本不需要,因为 MySetCtxComponent
确实如此不需要在上下文更新时渲染.但是,如果我从 useEffect
钩子的依赖数组中删除 myCtx
(这会在上下文更新时阻止效果钩子),那么我会收到一个 es-lint 警告,例如因为 React Hook useEffect 缺少依赖项:'myCtx'.要么包含它,要么删除依赖数组 react-hooks/exhaustive-deps
.
最后的问题是:在这种情况下,忽略警告是安全的,还是我在这里有一个基本的设计错误,也许应该选择使用商店?考虑到这个例子可能看起来很傻,但它是真实场景的最精简版本.
这里是一个stackblitz复制案例
解决这个问题的一种模式是将上下文一分为二,为操作提供一个上下文,另一个用于访问上下文值.这使您可以正确实现 useEffect 的预期依赖项数组,同时也不会在仅上下文值发生更改时不必要地运行它.
const { useState, createContext, useContext, useEffect, useRef } = React;const ViewContext = createContext();const ActionsContext = createContext();函数 MyContainer() {const [contextState, setContextState] = useState();返回 (<ViewContext.Provider value={contextState}><ActionsContext.Provider value={setContextState}><MySetCtxComponent/><MyViewCtxComponent/></ActionsContext.Provider></ViewContext.Provider>)}函数 MySetCtxComponent() {const setContextState = useContext(ActionsContext);const 计数器 = useRef(0);useEffect(() => {console.log("========>>>>>>>>>>>> 在 MySetCtxComponent 中使用效果运行");const intervalID = setInterval(() => {setContextState("新值" + counter.current);counter.current++;}, 1000);返回 () =>清除间隔(间隔ID);}, [setContextState]);return
);}ReactDOM.render(<我的容器/>,document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script><div id="root"></div>
I have a component MyContainer
which has a state variable (defined via useState
hook), defines a context provider to which it passes the state variable as value and contains also 2 children, MySetCtxComponent
and MyViewCtxComponent
.
MySetCtxComponent
can change the value stored in the context invoking a set function which is also passed as part of the context, BUT DOES NOT RENDER it.
MyViewCtxComponent
, on the contrary, RENDERS the value stored in the context.
MySetCtxComponent
defines also an effect via useEffect
hook. This effect is, for instance, used to update the value of the context at a fixed interval of time.
So the code of the 3 components is this
MyContainer
export function MyContainer() {
const [myContextValue, setMyContextValue] = useState<string>(null);
const setCtxVal = (newVal: string) => {
setMyContextValue(newVal);
};
return (
<MyContext.Provider
value={{ value: myContextValue, setMyContextValue: setCtxVal }}
>
<MySetCtxComponent />
<MyViewCtxComponent />
</MyContext.Provider>
);
}
MySetCtxComponent (plus a global varibale to make the example simpler)
let counter = 0;
export function MySetCtxComponent() {
const myCtx = useContext(MyContext);
useEffect(() => {
console.log("=======>>>>>>>>>>>> Use Effect run in MySetCtxComponent");
const intervalID = setInterval(() => {
myCtx.setMyContextValue("New Value " + counter);
counter++;
}, 3000);
return () => clearInterval(intervalID);
}, [myCtx]);
return <button onClick={() => (counter = 0)}>Reset</button>;
}
MyViewCtxComponent
export function MyViewCtxComponent() {
const myCtx = useContext(MyContext);
return (
<div>
This is the value of the contex: {myCtx.value}
</div>
);
}
Now my problem is that, in this way, everytime the context is updated the effect of MySetCtxComponent
is run again even if this is not at all required since MySetCtxComponent
does not need to render when the context is updated. But, if I remove myCtx
from the dependency array of the useEffect
hook (which prevents the effect hook when the context get updated), then I get an es-lint warning such as React Hook useEffect has a missing dependency: 'myCtx'. Either include it or remove the dependency array react-hooks/exhaustive-deps
.
Finally the question: is this a case where it is safe to ignore the warning or do I have a fundamental design error here and maybe should opt to use a store? Consider that the example may look pretty silly, but it is the most stripped down version of a real scenario.
Here a stackblitz to replicate the case
One pattern for solving this is to split the context in two, providing one context for actions and another for accessing the context value. This allows you to fulfill the expected dependency array of the useEffect correctly, while also not running it unnecessarily when only the context value has changed.
const { useState, createContext, useContext, useEffect, useRef } = React;
const ViewContext = createContext();
const ActionsContext = createContext();
function MyContainer() {
const [contextState, setContextState] = useState();
return (
<ViewContext.Provider value={contextState}>
<ActionsContext.Provider value={setContextState}>
<MySetCtxComponent />
<MyViewCtxComponent />
</ActionsContext.Provider>
</ViewContext.Provider>
)
}
function MySetCtxComponent() {
const setContextState = useContext(ActionsContext);
const counter = useRef(0);
useEffect(() => {
console.log("=======>>>>>>>>>>>> Use Effect run in MySetCtxComponent");
const intervalID = setInterval(() => {
setContextState("New Value " + counter.current);
counter.current++;
}, 1000);
return () => clearInterval(intervalID);
}, [setContextState]);
return <button onClick={() => (counter.current = 0)}>Reset</button>;
}
function MyViewCtxComponent() {
const contextState = useContext(ViewContext);
return (
<div>
This is the value of the context: {contextState}
</div>
);
}
ReactDOM.render(
<MyContainer />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
这篇关于避免在 Context 更新时运行效果挂钩的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!