In my react app, I am rendering different instances of <Item>
Components and I want them to register/unregister in a Context depending if they are currently mounted or not.
I am doing this with two Contexts (ItemContext
provides the registered items, ItemContextSetters
provides the functions to 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>
);
}
The <Item>
Components should register themselves when they are mounted or their props.data
changes and unregister when they are unmounted. So I thought that could be done very cleanly with 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]);
...
}
Full example see this codesandbox
Now, the problem is that this gives me an infinite loop and I don't know how to do it better. The loop is happening like this (I believe):
- An
<Item>
callsregisterItem
- In the Context,
items
is changed and thereforeregisterItem
is re-built (because it depends on[items]
- This triggers a change in
<Item>
becauseitemContextSetters
has changed anduseEffect
is executed again. - Also the cleanup effect from the previous render is executed! (As stated here: "React also cleans up effects from the previous render before running the effects next time")
- This again changes
items
in the context - And so on ...
I really can't think of a clean solution that avoids this problem. Am I misusing any hook or the context api? Can you help me with any general pattern on how write such a register/unregister Context that is called by Components in their useEffect
-body and useEffect
-cleanup?
Things I'd prefer not to do:
- Getting rid of the context altogether. In my real App, the structure is more complicated and different components throughout the App need this information so I believe I want to stick to a context
- Avoiding the hard removal of the
<Item>
components from the dom (with{renderFirstBlock &&
) and use something like a stateisHidden
instead. In my real App this is currently nothing I can change. My goal is to trackdata
of all existing component instances.
Thank you!
itemContextSetters
is the name of youruseContext(ItemContextSetters)
. If that's the case, you shouldn't have it as part of youruseEffect
dependencies. – PerilymphReact Hook useEffect has a missing dependency: 'itemContextSetters'. Either include it or remove the dependency array. (react-hooks/exhaustive-deps)
. And for good reason! Since the effect is using itemContextSetters it must make sure to have the up-to-date version – CyrusitemContextSetters
. (Since it is constructed withuseCallback
and depends on[items]
there will be a new version of it every timeitems
changes) – Cyrus