AMS
AMS

Reputation: 133

What's a good pattern for invoking event handler props in useEffect?

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

Answers (1)

Ori Drori
Ori Drori

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

Related Questions