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:
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.
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.
@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?
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:
render
and then compares the result to the previous render.componentDidMount
and componentDidUpdate
during this phase.React organize work this way because it provides a couple of benefits:
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? :smile: Consider the following async rendering scenario:
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.
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.
Excellent answers. Thanks a lot for your help!
Edit: I'll sum up my own understanding for anybody reading:
componentDidMount
and unsubscriptions should happen in componentWillUnmount
(the former guarantees that the latter will happen).this.setState
within the commit phase (e.g. componentDidMount
, componentDidUpdate
) are synchronous.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.@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?
@NE-SmallTown
I test the
setState
in thecomponentDidMount
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 alert1
.
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
orcomponentDidUpdate
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).
@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!
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.
@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)
I'm not sure what you mean. I'm saying that we don't exit the top level frame before committing.
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.
@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!
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.
@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.
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'?
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?)
' 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:
setState
in cWM will cause this too because before render react has merged the state(i.e. the render method will only be called once)(https://jsfiddle.net/heaven_xz/7gzv7h50/36/).update state
(i.e. return new state) in gDSFP will cause the same result too. (https://jsfiddle.net/heaven_xz/7gzv7h50/35/)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.
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.
@bvaughn I think you mean componentDid*
:-)
Yikes! Thanks
Ok, maybe I need some time to understand this thread.
Thanks you guys @bvaughn @gaearon for the clarification! Thanks!
What's the meaning of "de-opting"?
However, it achieves correctness by sometimes de-opting to synchronous mode, obviating the benefits of async rendering.
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.
Most helpful comment
Hi đź‘‹
Conceptually, React does work in two phases:
render
and then compares the result to the previous render.componentDidMount
andcomponentDidUpdate
during this phase.React organize work this way because it provides a couple of benefits:
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
orcomponentDidUpdate
) in order to avoid potential memory leaks.How is all of this related to your question? :smile: Consider the following async rendering scenario:
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
orcomponentDidUpdate
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.