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!
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