Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subscribe incorrectly rendering fallback on initial mount #300

Closed
pkpbynum opened this issue Jun 9, 2023 · 5 comments
Closed

Subscribe incorrectly rendering fallback on initial mount #300

pkpbynum opened this issue Jun 9, 2023 · 5 comments

Comments

@pkpbynum
Copy link

pkpbynum commented Jun 9, 2023

Hi there, great library! I'm having an issue with the rendering lifecycle of Subscribe. In the core concepts you mention a drawback of shareReplay is that it keeps the last value around in memory--that is actually my desired behavior. e.g. if I navigate from page A -> B -> A, I'd like to hydrate A with the old data immediately and refetch new data in the background. I believe this is the model used by many other data fetching libraries (e.g. react query).

To work around this, I just piped a shareReplay into my observable, but Subscribe still isn't picking up on that latest value. On initial mount, it still renders the fallback component for a split second. A reproducible example is here.

Is there a way I can achieve this? Or is this a bug with Subscribe's initial state?

@pkpbynum pkpbynum changed the title Subscribe rendering fallback on initial mount Subscribe incorrectly rendering fallback on initial mount Jun 9, 2023
@josepot
Copy link
Member

josepot commented Jun 9, 2023

Is there a way I can achieve this? Or is this a bug with Subscribe's initial state?

Of course! Just keep a subscription always open, and then it will always have the latest value in memory. I.e: subscribe to the observable, right after defining it.

@voliva
Copy link
Collaborator

voliva commented Jun 10, 2023

<Subscribe> always guarantees that it will have subscribed to the source observables before any of its children is rendered. This is something that's needed to prevent lingering subscriptions, specially with React with concurrent mode.

As mentioned in the docs:

It will subscribe to all the observables used by its children before they get mounted, and will unsubscribe from all of them once it unmounts.
IMPORTANT: This Component first mounts itself rendering null, subscribes to source$ and then it renders its children.

The way it does this is by first rendering nothing (or the fallback) on initial render, subscribing to the sources, and then rendering the children passed to it.

Not sure if it needs more clarification on the docs, or maybe an explanation on the actual implications of this.

If you want to avoid this double-render you'll have to manage the subscriptions further up: Don't use <Subscribe> at that level, instead declare an observable that will subscribe to all the streams your components need (a merge usually does the trick) when they are needed to become active, and subscribe to this observable further up on your tree.

@pkpbynum
Copy link
Author

I think I understand the points you both are making. My point is that an observable with shareReplay should have it's last (aka "stale") value available synchronously in the initial mount, and the fallback should never be rendered. It should render the stale value until the next value is observed. This is the stale-while-revalidate paradigm. It seems to me that react-rxjs is doing nothing-while-revalidate. If this isn't the goal of this library I understand.

To @voliva's point, I'll try to raise my Subscribe up my react tree, but I think this isn't quite the behavior I want. If I do that, then the subscription's lifetime is no longer tied to where the data is needed in the react tree. Also--if I understand correctly--if there's an error, I'd need to remount that entire tree to start the subscription again.

@josepot
Copy link
Member

josepot commented Jun 12, 2023

My point is that an observable with shareReplay should have it's last (aka "stale") value available synchronously in the initial mount

Not necessarily, no. Just because an Observable has been enhanced with shareReplay, that doesn't guarantee that the Observable will emit synchronously. It will only emit synchronously if:

  1. It has previously emitted a value (while there were at least a one listener).
  2. After having emitted that first value at least one listener stayed subscribed by the time that the new subscription takes place.

However, the first value can come at any point in time, if it ever comes. So, yeah: just because an Observable has been created through the shareReplay enhancer, that doesn't mean that the observable will always emit synchronously.

My point is that an observable with shareReplay should have it's last (aka "stale") value available synchronously in the initial mount, and the fallback should never be rendered

Even if that was true (which it isn't), the only way to access that value is through a subscription, and since subscribing to an Observable it's a side-effect, it's something that can't be done on the render phase of the component. It can only be done after the component was mounted. That's b/c due to React concurrent-mode: a render function can be called on a component that will never be mounted (and thus never unmounted), and if that happen then we would be creating unnecessary side-effects and stale subscriptions. Therefore, there is no way to actually access the "current" value of a shareReplay Observable during the render function. That's why in this library we have had to come up with these StateObservables and other shenanigans, so that we can work around these short-comings (or trade offs, depends on who you ask) of React.

This is the stale-while-revalidate paradigm. It seems to me that react-rxjs is doing nothing-while-revalidate. If this isn't the goal of this library I understand.

We've most definitely failed at explaining the goals and the trade off of our library. Not your fault, of course. We should really improve our docs and explain these things better.

@voliva
Copy link
Collaborator

voliva commented Jun 13, 2023

To @voliva's point, I'll try to raise my Subscribe up my react tree, but I think this isn't quite the behavior I want. If I do that, then the subscription's lifetime is no longer tied to where the data is needed in the react tree. Also--if I understand correctly--if there's an error, I'd need to remount that entire tree to start the subscription again.

In theory it's posible to do the same behaviour you want, without relying on React's render cycle, but it's more tedious than just using <Subscribe> on the place you wanted.

<Subscribe> has this limitation that it will do that double render. In some cases that's not bad, but on others it is annoying... and the solution is to not use Subscribe there. You can move it to the nearest place where it doesnt get unmounted/remounted, and declare how that subscription is managed passing in the source$ prop.

I've modified your sandbox to better show you what I mean: https://codesandbox.io/s/elegant-feather-fw2vjs?file=/src/App.tsx
On these cases it gets more tedious, as React-RxJS currently can't really circumvent the limitations of React when it comes to execution of side-effects (such as creating a subscription).

If you need error handling, this is something you can also add to your custom subscription management. For this, retry({ delay: () => anotherObservable$ }) is usually very useful: It prevents the error from being propagated into the <Subscribe>, and re-subscribes to the observable when you declare it to.

Edit --- I forgot to mention that there's another alternative which sometimes can also work (in your sandbox it does). You can remove the fallback from Subscribe and just add a suspense boundary inside:

  const content = toggle ? null : (
    <Subscribe>
      <Suspense fallback="wait...">
        <TodoList />
      </Suspense>
    </Subscribe>
  );

Subscribe will still do the double render, but in this case it will render nothing on mount, so the user won't see the fallback "Wait..." for that split second. This is just to say most of the time this behaviour is not noticeable, but there are some cases where it does matter. For those ones, it's better to just declare how your subscriptions are managed through another observable.

@josepot josepot closed this as completed Jun 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants