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

What is meant within the README of create-subscription by async limitations? Can it be clarified? #13186

Closed
sebinsua opened this issue Jul 10, 2018 · 21 comments

Comments

@sebinsua
Copy link

sebinsua commented Jul 10, 2018

What is meant within the README.md of create-subscription by async limitations?

For full compatibility with asynchronous rendering, including both time-slicing and React Suspense, the suggested longer term solution is to move to one of the patterns described in the previous section.

The patterns described above are:

  • Redux/Flux stores should use the context API instead.
  • I/O subscriptions (e.g. notifications) that update infrequently should use simple-cache-provider instead.
  • Complex libraries like Relay/Apollo should manage subscriptions manually with the same techniques which this library uses under the hood (as referenced here) in a way that is most optimized for their library usage.

I don't think any of these suit our use case: a high performance WebSocket stream that produces price quotes which are rendered directly into components. The application domain is a realtime trading application for an investment bank I am consulting for.

Ideally, we want the price quotes to be passed straight into the component with as little ceremony as possible. This state will be transient, so:

  • I don't see why I need to use some kind of state management solution to store it somewhere.
  • I don't think I should need to use react#Context and to then pass the data down the tree, since I can just import the service wherever I want in my code and pass callbacks into this to begin receiving data. The latter seems simpler, with less ceremony and will make it easier to differentiate between different streams of price updates.

It seems to me that create-subscription is exactly what I need, however the comment about async limitations worries me. Is there something I'm missing? Could this be clarified in the README?

Is it because of priority? I think ideally we wish the price updates to be treated as if they are high priority, because we would prefer to decrease the likelihood of clients interacting with stale data.

@sebinsua sebinsua changed the title What is meant within the README of create-subscription by async limitations? What is meant within the README of create-subscription by async limitations? Can it be clarified? Jul 10, 2018
@aweary
Copy link
Contributor

aweary commented Jul 10, 2018

The proceeding paragraphs explain what the limitations are:

However, it achieves correctness by sometimes de-opting to synchronous mode, obviating the benefits of async rendering.

The effect of de-opting to sync mode is that the main thread may periodically be blocked (in the case of CPU-bound work), and placeholders may appear earlier than desired (in the case of IO-bound work).

What this means is that you might get some jank when processing subscription updates (since the main thread gets blocked) and if you're using Suspense the loading state might be rendered sooner than it would be otherwise.

I don't think any of these suit our use case

In your case, the third pattern should suite your use case

Complex libraries like Relay/Apollo should manage subscriptions manually with the same techniques which this library uses under the hood (as referenced here) in a way that is most optimized for their library usage.

You can always start using create-subscription if you feel it fits your use case, and then implement a custom solution if you end up needing more advanced control over your WebSocket subscriptions. You can use this as a reference for your own subscription component.

@sebinsua
Copy link
Author

sebinsua commented Jul 10, 2018

@aweary To be more clear my question is not does it de-opt, but what causes it to de-opt? I already read the README so I understand that it is limited and what the implications of this are. But I still do not understand what causes its limitations.

Within the advanced Gist there are various comments like it's only safe to unsubscribe during the commit phase. This could be related however create-subscription also does this, and in general the code looks very similar to me.

If I cannot understand what causes async de-opts I cannot write code which avoids this.

Edit: I just saw the link to react-streams down at the bottom of that Gist. Apparently @johnlindquist had a conversation with @bvaughn about ensuring react-streams is "async safe", however it's not clear to me why this package is able to be safe when create-subscription allegedly isn't?

@bvaughn
Copy link
Contributor

bvaughn commented Jul 10, 2018

Hi 👋

To be more clear my question is not does it de-opt, but what causes it to de-opt?

Conceptually, React does work in two phases:

  • The render phase determines what changes need to be made to e.g. the DOM. During this phase, React calls render and then compares the result to the previous render.
  • The commit phase is when React applies any changes. (In the case of React DOM, this is when React inserts, updates, and removes DOM nodes.) React also calls lifecycles like componentDidMount and componentDidUpdate during this phase.

React organize work this way because it provides a couple of benefits:

  • If an error happens while rendering a component, React can safely throw away any of the in-progress work and let an Error Boundary decide what to render.
  • If there are a lot of components to render, React can split the work up to be processed in smaller chunks in order to avoid blocking the browser. Once all components have rendered, React can (synchronously) commit the work and e.g. update the DOM. (This is the gist of the new experimental async rendering mode!)
  • React can prioritize work. If higher priority work is scheduled while lower priority work is in progress, React can safely set aside the lower priority work for later and start working on the higher priority work instead. Since React only applies updates to e.g. the DOM during the commit phase, it never has to worry about leaving an application in a partially updated (broken) state.

In order for React to safely leverage these benefits it is important that components do not cause side effects during the render phase. This includes subscribing to something (e.g. adding an event handler). Adding a subscription should only be done in the commit phase (componentDidMount or componentDidUpdate) in order to avoid potential memory leaks.

How is all of this related to your question? 😄 Consider the following async rendering scenario:

  1. Your application has an event dispatcher that components subscribe to.
  2. A new component is created, and this event dispatcher / target is passed as a prop.
  3. When the component first renders, it reads the current value of the target and uses it.
  4. React has a lot of components to process as part of the current render, and so it yields before completing the work.
  5. During this yielded time, the target dispatches a new value. (Since your component is not yet subscribed, it is not notified of this value.)
  6. React later resumes the rendering work, finishes, and commits it.
  7. Your component subscribes to the target in componentDidMount but has already missed the event that was dispatched between the render and subsequent commit.

create-subscription handles this possible case by checking if the value that was rendered is out of sync with the latest value and scheduling a new render by calling setState if it is. This ensures that your component doesn't display stale data.

State updates scheduled from componentDidMount or componentDidUpdate are processed synchronously and flushed before the user sees the UI update. This is important for certain use cases (e.g. positioning a tooltip after measuring a rendered DOM element). In the case we're describing, this means that users of your application will never even see the temporary stale value because React will process the new value (synchronously) before yielding.

That might sound like a good thing, but what if the re-render includes a lot of components or is slow for some other reason? Then it might impact the frame rate and cause your application to feel unresponsive. This is what we are referring to when we say that create-subscription de-opts to synchronous rendering mode in some cases.

@aweary
Copy link
Contributor

aweary commented Jul 10, 2018

To circle back to your initial point of confusion @sebinsua, I think the language in the README is just slightly confusing. create-subscription and the advanced template both use the same techniques to maintain correctness with async rendering, so they de-opt in the same cases.

With this section:

For full compatibility with asynchronous rendering, including both time-slicing and React Suspense, the suggested longer term solution is to move to one of the patterns described in the previous section.

It's mainly referring to the other two, more common uses cases (external state store, low-frequency I/O subscriptions). If you opt to use that template you'll have more control over the subscription with the same async-safety. If you don't need that control you can just use create-subscription.

it's not clear to me why this package is able to be safe when create-subscription allegedly isn't

create-subscription is async safe. It just de-opts to synchronous rendering in some scenarios in order to maintain that safety.

@sebinsua
Copy link
Author

sebinsua commented Jul 10, 2018

Excellent answers. Thanks a lot for your help!

Edit: I'll sum up my own understanding for anybody reading:

  • Don't cause side-effects in the render-phase since there are no guarantees how often methods within this phase will be run. Basically, in order to avoid memory leaks, subscriptions should be created in componentDidMount and unsubscriptions should happen in componentWillUnmount (the former guarantees that the latter will happen).
  • Calls to this.setState within the commit phase (e.g. componentDidMount, componentDidUpdate) are synchronous.
  • Therefore create-subscription and the advanced template mentioned above will de-opt to synchronous mode when a value from a subscription became stale while rendering, and the Subscription component was just (a) mounted or (b) updated with a new source. This behaviour seems to exists to correct situations in which components yield so that other components can be rendered and then later on resume rendering with stale data.

@NE-SmallTown
Copy link
Contributor

NE-SmallTown commented Aug 6, 2018

@sebinsua

Calls to this.setState within the commit phase (e.g. componentDidMount, componentDidUpdate) are synchronous.

I test the setState in the componentDidMount base on [email protected] or local build. Both them are asynchronous(i.e. state = { foo: 1 }; componentDidMount() { this.setState({ foo: 2 }); alert(this.state.foo) }) will still alert 1.

@bvaughn @gaearon IIRC, suspense/async render won't change the setState behavior in the commit lifecycle, e.g. before the setState is async, it won't be synchronous even on async/render. Test on local build react and ReactSuspense-test.internal.js and ReactDOMFiberAsync-test.internal.js. If not, could you give me the unit test case link? Thanks.

@bvaughn

create-subscription handles this possible case by checking if the value that was rendered is out of sync with the latest value and scheduling a new render by calling setState if it is. This ensures that your component doesn't display stale data.

Hi, brian, I have a little question here. If so, we can't response to the transition of the data changing.

E.g. the event dispatcher dispatch 'foo', and then dispatch 'bar', and then dispatch 'bar2', we can just receive the 'bar2' at the end(due to 'foo' !== 'bar2'), but if we want to do something when data === 'bar', we will missing this action because create-subscription doesn't have a data changing list internal, it just has a value internal. So, in this situation, the data is still 'stale'.

If I'am wrong, please let me know, thanks!

By the way, what does 'de-opts' and 'de-opting' mean?

@sebinsua
Copy link
Author

sebinsua commented Aug 6, 2018

@NE-SmallTown

I test the setState in the componentDidMount base on [email protected] and local build. Both them are asynchronous(i.e. state = { foo: 1 }; componentDidMount() { this.setState({ foo: 2 }); alert(this.state.foo) }) will still alert 1.

I could be wrong, but I think what is meant by synchronous here, isn't what you are suggesting. I think the idea is that calling setState at this point guarantees that the state which you are updating with will be the state which is flushed to the screen. It's intention appears to be correctness.

To quote @bvaughn:

State updates scheduled from componentDidMount or componentDidUpdate are processed synchronously and flushed before the user sees the UI update. This is important for certain use cases (e.g. positioning a tooltip after measuring a rendered DOM element). In the case we're describing, this means that users of your application will never even see the temporary stale value because React will process the new value (synchronously) before yielding.

It does not mean that immediately after calling this.setState that this.state will have been updated.

By the way, what does 'de-opts' and 'de-opting' mean?

Presumably de-optimises (where the optimisation which is being lost is asynchronicity).

@NE-SmallTown
Copy link
Contributor

NE-SmallTown commented Aug 6, 2018

@sebinsua

I think the idea is that calling setState at this point guarantees that the state which you are updating with will be the state which is flushed to the screen. It's intention appears to be correctness.

Even in async mode, 'the state which you are updating with will be the state which is flushed to the screen' is still applicable. And I think the 'correctness' is only about asynchronous because synchronous is always correct.

IMO, the 'synchronous' here which @bvaughn want to say is, the commit phase method will only be called once, no suspend, so we call it 'synchronous', by contrast, async render/suspense will suspend/yield the render phase method which would cause these methods be called than once maybe. so we call it 'asynchronous'.

It does not mean that immediately after calling this.setState that this.state will have been updated.

Yea, I thinks so. So maybe we need adjust the description 'Calls to this.setState within the commit phase (e.g. componentDidMount, componentDidUpdate) are synchronous.' a little?

@bvaughn @gaearon If I'm wrong, please let me know, thanks!

@gaearon
Copy link
Collaborator

gaearon commented Aug 6, 2018

setState in componentDidMount or componentDidUpdate guarantees that the user will not see the previous state. It’s not technically “synchronous” in the sense of flushing during setState but it is synchronous in the sense that it will be flushed before React exits the top-level JavaScript call stack.

@NE-SmallTown
Copy link
Contributor

@gaearon Thanks for your clarification dan! Just one question,IIRC, it seems it's not 'the top-level JavaScript call stack' but 'the componentDidMount or componentDidUpdate call stack' (https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberCommitWork.js#L267-L278)

@gaearon
Copy link
Collaborator

gaearon commented Aug 6, 2018

I'm not sure what you mean. I'm saying that we don't exit the top level frame before committing.

@bvaughn
Copy link
Contributor

bvaughn commented Aug 6, 2018

Thanks for clarifying Dan. My wording was a little misleading I guess.

State updates from componentDidMount or componentDidUpdate are processed before React finishes its batch of work and gives the browser time to paint the updates. In other words, you'll never actually see UI for the original update if you call setState from either of those methods– only the UI the state update results in.

Here is an example:

class Example extends React.Component {
  state = {
    count: 0
  };

  componentDidMount() {
    // Don't actually do this!
    this.setState({ count: 1 });
  }

  componentDidUpdate(prevProps, prevState) {
    if (this.state.count === 2 && prevState.count === 1) {
      // Don't actually do this!
      this.setState({ count: 3 });
    }
  }

  render() {
    return <div>count: {this.state.count}</div>;
  }
}

The above component will render count 0 and 2, but a user would never actually see those counts– only 1 and 3, because of the setState calls.

@NE-SmallTown
Copy link
Contributor

NE-SmallTown commented Aug 6, 2018

@gaearon Sorry, my fault, I mistakenly treat the word 'top' as 'bottom'. Our understanding is identical.

@bvaughn Yea, that's how batch update works. But I think this is irrelative with create-subscription or async/render or this thread because even in react@15, it already has batch update and async render is about fiber, chunk, time-slicing, expriationTime ...

By the way, could you please answer the second question in my comment above? Thanks!

@bvaughn
Copy link
Contributor

bvaughn commented Aug 6, 2018

I'm confused by your most recent comment, @NE-SmallTown. My example above doesn't really have anything to do with batch rendering (as there's only ever one state update in the queue at a time). I was just trying to clarify about how state updates are handled during the "commit" phase.

Not sure I know what second question you're referring to.

@NE-SmallTown
Copy link
Contributor

NE-SmallTown commented Aug 7, 2018

@bvaughn

Yes, there is no multiple update at a time. My wrong description. I will try to describe my thoughts more clear. I'm very very sorry about wasting your time !

State updates from componentDidMount or componentDidUpdate are processed before React finishes its batch of work and gives the browser time to paint the updates. In other words, you'll never actually see UI for the original update if you call setState from either of those methods– only the UI the state update results in.

  1. What does 'before' mean? IMO, only we have executed/called the cDM/cDU method, we know how the state change finally, so I think it can't be 'before', but 'after'?

  2. What does 'its batch of work' actually mean('batch' means the work be batched rather than update be batched? 'work' means fiber beginWork and commitWork or something else in your example code?)

  3. ' if you call setState from either of those methods– only the UI the state update results in.', IMO, this result is not special for those methods(i.e. cDM/cDU). E.g:

  • 'gives the browser time to paint the updates'
  • 'you'll never actually see UI for the original update'
    • So I think this is not about calling setState in which method(or whether in commit phase), but about how to reduce the interval between two/three/four... updates or how to reduce the update times/merge updates to make the browser can paint quickly.

Not sure I know what second question you're referring to.

Sorry for the ambiguous, you can try command + f to search the key 'I have a little question here' on this page.

@bvaughn
Copy link
Contributor

bvaughn commented Aug 7, 2018

I'm having a little trouble understanding what you're asking, sorry.

All I am saying, essentially, is this: Calling this.setState from either componentDidMount or componentDidUpdate will cause another render but the user will not see it (or know about it) because React will do this work synchronously before finishing execution.

As someone pointed out above, React does not update this.state synchronously when you call this.setState but it will synchronously re-render the component after componentDidMount or componentDidUpdate is finished running.

@gaearon
Copy link
Collaborator

gaearon commented Aug 7, 2018

@bvaughn I think you mean componentDid* :-)

@bvaughn
Copy link
Contributor

bvaughn commented Aug 7, 2018

Yikes! Thanks

@NE-SmallTown
Copy link
Contributor

NE-SmallTown commented Aug 8, 2018

Ok, maybe I need some time to understand this thread.

Thanks you guys @bvaughn @gaearon for the clarification! Thanks!

@towry
Copy link

towry commented Oct 9, 2020

What's the meaning of "de-opting"?

However, it achieves correctness by sometimes de-opting to synchronous mode, obviating the benefits of async rendering.

@bvaughn
Copy link
Contributor

bvaughn commented Oct 9, 2020

What's the meaning of "de-opting"?

@towry In the context you're referring to, it just means that React can't continue rendering asynchronously (which is ideal). In some cases with create-subscription it has to fall back to rendering synchronously to keep the view consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants