React:在useEffect中调用上下文函数时防止无限循环
React: Prevent infinite Loop when calling context-functions in useEffect
在我的 React 应用程序中,我正在渲染 <Item>
组件的不同实例,我希望它们 register/unregister 在上下文中,具体取决于它们当前是否已安装。
我正在使用两个上下文(ItemContext
提供注册项,ItemContextSetters
提供 register/unregister 的功能)。
const ItemContext = React.createContext({});
const ItemContextSetters = React.createContext({
registerItem: (id, data) => undefined,
unregisterItem: (id) => undefined
});
function ContextController(props) {
const [items, setItems] = useState({});
const unregisterItem = useCallback(
(id) => {
const itemsUpdate = { ...items };
delete itemsUpdate[id];
setItems(itemsUpdate);
},
[items]
);
const registerItem = useCallback(
(id, data) => {
if (items.hasOwnProperty(id) && items[id] === data) {
return;
}
const itemsUpdate = { ...items, [id]: data };
setItems(itemsUpdate);
},
[items]
);
return (
<ItemContext.Provider value={{ items }}>
<ItemContextSetters.Provider value={{ registerItem, unregisterItem }}>
{props.children}
</ItemContextSetters.Provider>
</ItemContext.Provider>
);
}
<Item>
组件应在安装时自行注册,或在卸载时取消注册 props.data
更改。所以我认为可以用 useEffect
:
非常干净地完成
function Item(props) {
const itemContextSetters = useContext(ItemContextSetters);
useEffect(() => {
itemContextSetters.registerItem(props.id, props.data);
return () => {
itemContextSetters.unregisterItem(props.id);
};
}, [itemContextSetters, props.id, props.data]);
...
}
完整示例见此codesandbox
现在,问题是 这给了我一个无限循环,我不知道如何做得更好。循环是这样发生的(我相信):
- 一个
<Item>
调用 registerItem
- 在上下文中,
items
被改变,因此registerItem
被重建(因为它依赖于[items]
- 这会触发
<Item>
中的更改,因为 itemContextSetters
已更改并且 useEffect
会再次执行。
- 还执行了之前渲染的清理效果! (如前所述 here:“React 还会在 运行 下一次效果之前清除上一次渲染的效果”)
- 这再次改变了上下文
items
- 等等...
我真的想不出一个干净的解决方案来避免这个问题。我是否滥用了任何钩子或上下文 api?你能帮我写出这样一个 register/unregister 上下文的一般模式吗,这个 register/unregister 上下文被组件调用在它们的 useEffect
-body 和 useEffect
-cleanup 中?
我不想做的事情:
- 完全摆脱上下文。在我的真实应用程序中,结构更复杂,整个应用程序中的不同组件都需要这些信息,所以我相信我想坚持一个上下文
- 避免从 dom(使用
{renderFirstBlock &&
)中硬删除 <Item>
组件,而是使用状态 isHidden
之类的东西。在我的真实应用程序中,目前我无能为力。我的目标是跟踪所有现有组件实例的 data
。
谢谢!
您可以使您的 setter 具有稳定的引用,因为它们实际上并不需要依赖于 items
:
const ItemContext = React.createContext({});
const ItemContextSetters = React.createContext({
registerItem: (id, data) => undefined,
unregisterItem: (id) => undefined
});
function ContextController(props) {
const [items, setItems] = useState({});
const unregisterItem = useCallback(
(id) => {
setItems(currentItems => {
const itemsUpdate = { ...currentItems };
delete itemsUpdate[id];
return itemsUpdate
});
},
[]
);
const registerItem = useCallback(
(id, data) => {
setItems(currentItems => {
if (currentItems.hasOwnProperty(id) && currentItems[id] === data) {
return currentItems;
}
return { ...currentItems, [id]: data }
} );
},
[]
);
const itemsSetters = useMemo(() => ({ registerItem, unregisterItem }), [registerItem, unregisterItem])
return (
<ItemContext.Provider value={{ items }}>
<ItemContextSetters.Provider value={itemsSetters}>
{props.children}
</ItemContextSetters.Provider>
</ItemContext.Provider>
);
}
现在你的效果应该可以正常工作了
在我的 React 应用程序中,我正在渲染 <Item>
组件的不同实例,我希望它们 register/unregister 在上下文中,具体取决于它们当前是否已安装。
我正在使用两个上下文(ItemContext
提供注册项,ItemContextSetters
提供 register/unregister 的功能)。
const ItemContext = React.createContext({});
const ItemContextSetters = React.createContext({
registerItem: (id, data) => undefined,
unregisterItem: (id) => undefined
});
function ContextController(props) {
const [items, setItems] = useState({});
const unregisterItem = useCallback(
(id) => {
const itemsUpdate = { ...items };
delete itemsUpdate[id];
setItems(itemsUpdate);
},
[items]
);
const registerItem = useCallback(
(id, data) => {
if (items.hasOwnProperty(id) && items[id] === data) {
return;
}
const itemsUpdate = { ...items, [id]: data };
setItems(itemsUpdate);
},
[items]
);
return (
<ItemContext.Provider value={{ items }}>
<ItemContextSetters.Provider value={{ registerItem, unregisterItem }}>
{props.children}
</ItemContextSetters.Provider>
</ItemContext.Provider>
);
}
<Item>
组件应在安装时自行注册,或在卸载时取消注册 props.data
更改。所以我认为可以用 useEffect
:
function Item(props) {
const itemContextSetters = useContext(ItemContextSetters);
useEffect(() => {
itemContextSetters.registerItem(props.id, props.data);
return () => {
itemContextSetters.unregisterItem(props.id);
};
}, [itemContextSetters, props.id, props.data]);
...
}
完整示例见此codesandbox
现在,问题是 这给了我一个无限循环,我不知道如何做得更好。循环是这样发生的(我相信):
- 一个
<Item>
调用registerItem
- 在上下文中,
items
被改变,因此registerItem
被重建(因为它依赖于[items]
- 这会触发
<Item>
中的更改,因为itemContextSetters
已更改并且useEffect
会再次执行。 - 还执行了之前渲染的清理效果! (如前所述 here:“React 还会在 运行 下一次效果之前清除上一次渲染的效果”)
- 这再次改变了上下文
items
- 等等...
我真的想不出一个干净的解决方案来避免这个问题。我是否滥用了任何钩子或上下文 api?你能帮我写出这样一个 register/unregister 上下文的一般模式吗,这个 register/unregister 上下文被组件调用在它们的 useEffect
-body 和 useEffect
-cleanup 中?
我不想做的事情:
- 完全摆脱上下文。在我的真实应用程序中,结构更复杂,整个应用程序中的不同组件都需要这些信息,所以我相信我想坚持一个上下文
- 避免从 dom(使用
{renderFirstBlock &&
)中硬删除<Item>
组件,而是使用状态isHidden
之类的东西。在我的真实应用程序中,目前我无能为力。我的目标是跟踪所有现有组件实例的data
。
谢谢!
您可以使您的 setter 具有稳定的引用,因为它们实际上并不需要依赖于 items
:
const ItemContext = React.createContext({});
const ItemContextSetters = React.createContext({
registerItem: (id, data) => undefined,
unregisterItem: (id) => undefined
});
function ContextController(props) {
const [items, setItems] = useState({});
const unregisterItem = useCallback(
(id) => {
setItems(currentItems => {
const itemsUpdate = { ...currentItems };
delete itemsUpdate[id];
return itemsUpdate
});
},
[]
);
const registerItem = useCallback(
(id, data) => {
setItems(currentItems => {
if (currentItems.hasOwnProperty(id) && currentItems[id] === data) {
return currentItems;
}
return { ...currentItems, [id]: data }
} );
},
[]
);
const itemsSetters = useMemo(() => ({ registerItem, unregisterItem }), [registerItem, unregisterItem])
return (
<ItemContext.Provider value={{ items }}>
<ItemContextSetters.Provider value={itemsSetters}>
{props.children}
</ItemContextSetters.Provider>
</ItemContext.Provider>
);
}
现在你的效果应该可以正常工作了