Observable in hook dependency array causing infinite loop

Viewed 240

I have a custom hook which receives an array of Observables as a parameter, then (in a useEffect hook) sets internal state any time the Observable emits a value.

export const myHook = (obs$: Observable<any>) => {
   const [state, setState] = useState();

   useEffect(() => {
      obs$.subscribe((x) => setState(x));
   }, [obs$]);
 
   return state;
}

This causes an infinite loop, I'm guessing because some property on the obs$ object is changing as it resolves. It works with an empty dependency array, but for the sake of correctness I don't want to do that because it should recalculate if obs$ is genuinely changed outside of the scope of the custom hook (not sure why this would ever happen, but still).

1 Answers

Observables are objects. The following expression is false: {} === {} (similarly for == and other "similar" objects) (more info here).

React's useEffect hook uses === to compare dependencies to see if they've changed. Since each obs$ has different references, you'll need to find a way to ensure that the same exact object reference is passed to your hook. If the === comparator returns true, then the useEffect won't run again.

That begs the question: How do we ensure that the obs$ has the same reference unless it changes?

There are many ways, but useMemo and useCallback are tools that can help with this. Each of those have their own dependencies, but they shouldn't be used in your hook since you'll have the same problem of needing to depend on the same obs$ that changes with each function call.

If obs$ has an unsubscribe method, then I would unsubscribe when the component re-renders (or unmounts).

export const useHook = (obs$: Observable<any>) => { // hooks should start with the `use` prefix
   const [state, setState] = useState();

   useEffect(() => {
      obs$.subscribe((x) => setState(x));

      return () => obs$.unsubscribe() // return a cleanup function
   });
 
   return state;
}

In the React docs, there is a similar example.

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // Clean up the subscription
    subscription.unsubscribe();
  };
});
Related