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):
<Item>
calls registerItem
items
is changed and therefore registerItem
is re-built (because it depends on [items]
<Item>
because itemContextSetters
has changed and useEffect
is executed again.items
in the contextI 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:
<Item>
components from the dom (with {renderFirstBlock &&
) and use something like a state isHidden
instead. In my real App this is currently nothing I can change. My goal is to track data
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
:
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>
);
}
Now your effect should work as expected