About React
Published by powerfulyang on Jan 12, 2023
React18 的一些变化
- Update to allow components to render undefined
- Automatic batching for fewer renders in React 18
- New in 18: useEffect fires synchronously when it's the result of a discrete input
groups multiple state updates into a single re-render
This is because React used to only batch updates during a browser event (like click), but here we’re updating the state after the event has already been handled.
- React17 和之前的版本只会 browser event 的同步过程中执行 batch updates。
- React18 版本中使用
createRoot
, 所用更新会自动合并。This means that updates inside of timeouts, promises, native event handlers or any other event will batch the same way as updates inside of React events. We expect this to result in less work rendering, and therefore better performance in your applications
Note: React only batches updates when it’s generally safe to do. For example, React ensures that for each user-initiated event like a click or a keypress, the DOM is fully updated before the next event. This ensures, for example, that a form that disables on submit can’t be submitted twice. 所以用 state 来展示按钮的 loading 状态是可行的。
flushSync
Usually, batching is safe, but some code may depend on reading something from the DOM immediately after a state change. For those use cases, you can use ReactDOM.flushSync()
to opt out of batching.
历史陋习不再有用
Class components had an implementation quirk where it was possible to synchronously read state updates inside of events. This means you would be able to read this.state
between the calls to setState
:
1handleClick = () => {
2 setTimeout(() => {
3 this.setState(({ count }) => ({ count: count + 1 }));
4
5 // { count: 1, flag: false }
6 console.log(this.state);
7
8 this.setState(({ flag }) => ({ flag: !flag }));
9 });
10};
In React 18, this is no longer the case. Since all of the updates even in setTimeout
are batched, React doesn’t render the result of the first setState
synchronously—the render occurs during the next browser tick. So the render hasn’t happened yet:
1handleClick = () => {
2 setTimeout(() => {
3 this.setState(({ count }) => ({ count: count + 1 }));
4
5 // { count: 0, flag: false }
6 console.log(this.state);
7
8 this.setState(({ flag }) => ({ flag: !flag }));
9 });
10};
If this is a blocker to upgrading to React 18, you can use ReactDOM.flushSync
to force an update, but we recommend using this sparingly:
1handleClick = () => {
2 setTimeout(() => {
3 ReactDOM.flushSync(() => {
4 this.setState(({ count }) => ({ count: count + 1 }));
5 });
6
7 // { count: 1, flag: false }
8 console.log(this.state);
9
10 this.setState(({ flag }) => ({ flag: !flag }));
11 });
12};
- Updates queued in the commit phase (
componentDidMount
/componentDidUpdate
/useLayoutEffect
) are executed synchronously - Updates queued in the passive effects phase (
useEffect
callbacks) are deferred to the end of the effects phase- Effects originating from intentional user-initiated events (like clicks, key presses, etc — we call them discrete, here's the list) now run synchronously at the end of the current task (facebook/react#21150). This gives them consistent timing. In Legacy roots, they are unfortunately inconsistent and we can't fix that (facebook/react#20074 (comment)). So I would expect setting state from such effects to also be synchronous.
- Updates queued in passive effects are scheduled to run at different priorities after all passive effects run.
- The priority they're scheduled at depends on how the original update(s) were scheduled. Most of the time, updates inside of passive effects are flushed synchronously in a task with normal priority, at the latest. This is true even if the original update was inside a discrete event, flush sync, a timeout, promise tick, or transition.
- The only time this is different (currently) is in offscreen trees. Those passive effects are scheduled in a task at Idle priority.
- The "passive effect phase" ends when all effects have run. During that phase, updates may be scheduled, but they will run after the passive effect phase ends, according to their priority and how they were scheduled.
过去存在的误解
1setState(1);
2
3setTimeout(() => {
4 setState(2)
5}, 0)
6// 过去认为会批量更新
The updates outside setTimeout
will be batched together and updates inside setTimeout
will be batched together separately so it will be 2 separate renders.
As far as I understand since the code inside setTimeout
is asynchronous and will be executed after some delay(until event loop pops from the task queue and pushes to the call stack) hence React will not be batching it.
I know I also saw a mention of "5ms" timing somewhere, but I think that was in reference to how long React will work before yielding back to the browser, and I may have misinterpreted that as "React will batch updates across ticks that occur within 5ms of each other".
I am not sure about the 5ms timing, but batching isn't related to queuing within 5ms as far as I understand, batching makes sure that app isn't rendered until it executes the whole callback. But If we use the concurrent api startTransition
instead of setTimeout
then the startTransition
will be executed immediately and the updates inside it will be marked as transition which could be interrupted incase of some urgent update (click) kicking in.
discrete input
In React 17 and below, the function passed to useEffect typically fires asynchronously after the browser has painted. The idea is to defer as much work as possible until paint so that the user experience is not delayed. This includes things like setting up subscriptions.
For example, imagine you're building a form that disables "submit" after the first submission:
1useEffect(() => {
2 if (!disableSubmit) {
3 const form = formRef.current;
4 form.addEventListener('submit', onSubmit);
5 return () => {
6 form.removeEventListener('submit', onSubmit);
7 };
8 }
9}, [disableSubmit, onSubmit]);
If the user attempts to submit the form multiple times in quick succession, we need to make sure the effects from the first event have completely finished before the next input is processed. So we synchronously flush them.
We don't need to do this for events that aren't discrete, because we assume they are not order-dependent and do not need to be observed by external systems.
似乎其实用 useLayoutEffect 也就行了,但是 useLayoutEffect 会让其导致的更新也会同步执行。
Note that this only affects the timing of when useEffect functions are called. It does not affect the priority of updates that are triggered inside a useEffect. They still get the default priority. (This is in contrast to useLayoutEffect, which not only calls the effect callback synchronously but also gives synchronous priority to its updates.)