Reputation: 133
Let's assume a component:
const Foo = ({id, onError}) => {
useEffect(() => {
subscribe(id).catch(error => onError(error));
return () => cleanup(id);
}, [id, onError]);
return <div>...</div>;
}
The idea is simple-- run an effect that subscribes using the current "id". If the subscription fails, invoke an event handler onError
that is passed down as a prop.
However, for this to work correctly, the onError
prop that's passed down must be referentially stable. In other words, if a consumer of my component tried the following, they may run into problems where the effect is run for each render:
const Parent = () => {
// handleError is recreated for each render, causing the effect to run each time
const handleError = error => {
console.log("error", error);
}
return <Foo id="test" onError={handleError} />
}
Instead, they would need to do something like:
const Parent = () => {
// handleError identity is stable
const handleError = useCallback(error => {
console.log("error", error);
},[]);
return <Foo id="test" onError={handleError} />
}
This works, but I'm somehow unhappy with it. The consumers of Foo
need to realize that onError
must be stable, which is not something that's plainly obvious unless you look at its underlying implementation. It breaks the component encapsulation and consumers can easily run into problems without realizing it.
Is there a better pattern to manage props like event handlers that may be invoked within useEffect
?
Upvotes: 3
Views: 198
Reputation: 191946
You need to remove onError
from your dependency list, but still call if it changes. For that you can use a ref, and update it via useEffect
on each render.
You can also use optional chaining ?.
to avoid invoking the function if it's undefined
.
const Foo = ({ id, onError }) => {
const onErrorRef = useRef();
useEffect(() => {
onErrorRef.current = onError;
});
useEffect(() => {
subscribe(id).catch(error => onErrorRef.current?.(error));
return () => cleanup(id);
}, [id]);
return <div>...</div>;
}
Upvotes: 2