crimx/observable-hooks

[Question] Why useMemo and useRef instead of useEffect?

rassie opened this issue · 7 comments

I'm sorry for hijacking issues for a question, but I've struggled to find an explanation on my own and I think that this might be useful for other people.

On more than one occasion, Google search produces a common pattern for subscribing to observables in React context:

React.useEffect(() => {
  const sub = observable$.subscribe();
  return () => sub.unsubscribe();
}, [])

In most cases, this works fine, but sometimes, it fails spectacularly. For example, I've encountered a situation in which

React.useEffect(() => {
  const sub = combineLatest(o1$, o2$, o3$, o4$).subscribe();
  return () => sub.unsubscribe();
}, [])

it getting initialized, but somehow combineLatest fails to subscribe to the observables o1$ through o4$ properly, while doing combineLatest(o1$, o2$, o3$, o4$).subscribe() just outside the useEffect hook works as expected. I've tried creating a minimal test case for this situation, but failed, since most of the times, that useEffect pattern does work and I have not been able to figure out which parts of my observables make it fail.

Now, the epiphany: I've tried replacing my useEffects with useSubscription from observable-hooks and sure enough, my problematic code started working again. After I've looked into useSubscriptions code, I've noticed that useEffect is only used for unsubscription, subscription is handled via useMemo and useRef.

Are there any reasons why you implemented useSubscription this way? Is there any explanation why this combination works better that useEffect?

crimx commented

The reason of choosing useMemo is so that synchronous values from observables can be used as initial state without triggering an extra re-rendering.

But do note that this is safe for now but not safe in concurrent mode. I've been working on a concurrent mode compatible branch.

Back to your issue. It is probably one or more of your observables are hot and start emitting values before the component reaches commit phase where the subscription is performed.

I am also working on an api for this issue. Like returning an indicator showing the ready state of subscription.

Any comment is wellcome!

crimx commented

Can you test your code with useLayoutEffect and see if it works? I am trying to make this into the new version.

Thank you for the explanation! Happy to report that useSubscription provides exactly the behaviour I was expecting, useLayoutEffect has been about as effective as useEffect, i.e. not at all. Still not sure why my observables did not subscribe properly -- even though they are hot, I've been using shareReplay(1), so there should have been at least one initial next on the combined observable. Either way, observable-hooks has been the saviour for now, let's hope concurrent mode does not break a whole lot... :)

crimx commented

share* operators need at least one subscription to activate.

So more of a "warm" observable? But either way, combineLatest is supposed to provide those subscriptions, isn't it?

crimx commented

not really. combineLatest won't do anything until you subscribe.

crimx commented

Are you able to control the emission timing of the hot observables?

Just to make sure workaround exists for this type of issues before finishing the new version.