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,
itemsis changed and thereforeregisterItemis re-built (because it depends on[items] - This triggers a change in
<Item>becauseitemContextSettershas changed anduseEffectis 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
itemsin 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 stateisHiddeninstead. In my real App this is currently nothing I can change. My goal is to trackdataof all existing component instances.
Thank you!
You can make your setters have stable references, as they don't really need to be dependant on
items:Now your effect should work as expected