preface

In React18, whether a Concurrent mode is enabled or not depends on the context in which the update is triggered. React uses Legacy mode if updates are triggered in callback events, setTimeout, or Network Request, while updates related to Suspense, useTransition, or OffScreen React will adopt Concurrent mode.

UseTransition is a new API provided by Act18. With useTransition, the updated coordination is performed in interruptible Concurrent mode, which improves the user experience.

UseTransition We are going to talk about useTransition.

The directory structure of this article is as follows:

Anti-vibration, throttling VS useTransition

In daily development, you’ve probably all used the Select component with the search function. The Select component displays options that meet the criteria based on changes in the input value.

When the number of options that meet the conditions is small, the interaction of the Select component is smooth and the results can be quickly displayed to the user. However, when the number of options is large, the overall interaction is not very good.

Let’s use two simple demos to simulate the above scenario.

The number of options is small:

In the demo above, we defined 100 divs to simulate a situation where a small number of options meet the criteria. As shown, the whole interaction process is very smooth. With user input, corresponding results can be presented to the user in a timely manner.

However, when the number of options is large, the situation is not optimistic.

A large number of options:

In this demo, we defined 50,000 divs to simulate a situation where a large number of options meet the criteria. Compared to the previous demo, the interaction was very bad. When the user enters continuously, the whole page will be stuck. Only after the user stops typing, can he see the input content and the corresponding result list.

Let’s start with the domo performance diagram to see why the page freezes:

As can be seen from the performance analysis chart, the main thread of the browser has been occupied by the React update process and the browser layout process. As a result, the layer drawing cannot be completed, causing the page to freeze.

This is quite understandable, up to 50,000 nodes, which not only requires a lot of time to diff during update coordination, but also a lot of time to do layout during rendering, resulting in browsers not being able to render quickly and a poor user experience.

For this demo, we’ll modify it a little bit so that it doesn’t handle so many node-less-dom nodes during browser rendering:

In less-DOM, there are only 100 + nodes to deal with, but the overall experience is still poor. When the user continues to input, the page still freezes.

Let’s take a look at the corresponding performance analysis chart:

From the performance analysis chart, we can see that the reason for page gridlock this time is that the JS engine occupies the main thread of the browser for a long time due to the large number of nodes to coordinate during the update, which leads to the failure of the rendering engine.

For this type of situation, we usually optimise with anti-shock – debounce and throttle-throttle.

Debounce, throttle-throttle

Let’s start by using anti-shock -debounce to optimize.

Looking at the demo, we can see that with the use of debounce, the interaction improved significantly and there were no more page freezes. Anti-shock – debounce, so that updates triggered by successive inputs only actually start processing when the last input occurs. Until then, the browser’s rendering engine is not blocked and the user can see what is typed in time.

Anti-shock – debounce, essentially delays the React update operation. This approach has some disadvantages:

  • User input will not respond for a long time;
  • After the update operation is officially started, the rendering engine will still be blocked for a long time, and the page will still be jammed;

Throttle-throttle is often used to optimize for long periods of unresponsive user input.

After throttle-throttle optimization, users can not only see the input content in time, but also see the corresponding result list quickly, which greatly improves the overall interaction effect.

With throttle-throttle, however, there are still problems:

  • When the specified time is reached, the update is processed, the rendering engine is blocked for a long time, and page interactions stall;
  • The optimal time to throttle is typically configured based on the device the developer is using. And this configured time, for some poor equipment users, does not necessarily work, can not play the optimization effect.

To sum up, although anti-shake and throttling can improve the interactive effect to a certain extent, the page will still be stuck or even stuck. The reason is that fiber Tree coordination takes too long and cannot be interrupted during react update in the example, resulting in the JS engine occupying the main thread of the browser for a long time and causing the rendering engine to be blocked for a long time.

React18’s new API, useTransition, provides a new solution to the problem that fiber Tree coordination takes a long time and causes the rendering engine to block.

useTransition

When using useTransition react coordinates the Fiber Tree in Concurrent mode. In Concurrent mode, the coordination process is parallel and interruptible, and the rendering process will not be blocked for a long time, so that user operations can be timely responded, greatly improving user experience.

UseTransiton is used as follows:

In the demo above, we can see that using useTransition improves the overall interaction:

  • The user can see the input in real time and the interaction is smooth;
  • [Fixed] The user will no longer be unable to respond to continuous input (the render list must start updating within 5s at the latest);
  • Once you start updating the render, the coordination process is interruptible and does not block the render engine for long periods of time.

Finally, let’s make a brief summary. UseTransition has the following advantages over debounce and throttle-throttle:

  • The update coordination process is interruptible, the rendering engine is not blocked for a long time, and users can get timely responses;
  • There is no need for developers to do extra consideration, the whole optimization process is left to react and the browser;

Why useTransition can achieve similar debounce and Throtte effects will be explained in detail in section 3 – Principle analysis.

Using useTransition

UseTransition Hook and startTransition API useTransition Hook and startTransition API UseTransition hook is used for function components, startTransition is used for class components (of course, can also be used directly for function components).

useTransition

UseTransition is used as follows:

UseTransition Hook execution returns an isPending variable and startTransition method. React uses Concurrent mode to coordinate the Fiber Tree when the update is triggered by startTransiton(() => setState(XXX)).

React assigns a task for a Concurrent update every time it is triggered by setState. Different context triggers the update. As a result, the priorities of the generated tasks are different, and the corresponding tasks are processed in different order.

When an update is triggered using startTransition, the priority level of the update is NormalPriority. On top of NormalPriority, there are two higher level updates: ImmediatePriority and UserBlockingPriority. In general, high-priority updates are processed first, so that even though the transition update is triggered first, it is not processed first. Instead, it is in a pending state. It is processed only if no transition update exists with a higher priority than the transition update.

In this case, we can use the isPending returned by useTransition to display the status of the transition update. When isPending is true, the transition update has not yet been processed, so we can display an intermediate state to optimize the user experience. When isPending is false, the transition update is processed and the actual content is displayed.

UseTransition, as a hook, cannot be used in class components. In order to trigger transition updates in class components as well, Act18 provides another new API, startTransition.

Just use startTransition

Use startTransiton directly as follows:

StartTransition gives us a way to trigger transition updates directly, whether in a function component or a class component.

However, if startTransition is used directly, isPending cannot be provided like useTransition to display the state change of transition update.

In addition, we found an interesting phenomenon. StartTransition does not trigger throttle-like effects when we are typing rapidly in succession. The reasons for this will be explained in section 3.

The principle of analytic

Now that we know how to use the new useTransition and startTransition apis, let’s look at some of the internals involved in the Transition update.

In this section, we will analyze the following problems for you:

  • How does isPending work?
  • How does useTransition achieve a Debounce effect?
  • How does useTransition achieve throtte -like effects?
  • What is the difference between useTransition and startTransition?
  • What is the process of transition updating?

How does isPending work

When we use isPending and startTransition returned by useTransition, the pending intermediate state is displayed first, and then the transition update results are displayed.

Obviously, there were two React updates. But looking back at the code, we see that setState is only called once in startTransition, which means that only one React update should be triggered. So what’s going on here?

First of all, since there were two React updates, there must have been two triggers for the update. One update was triggered by setState ourselves in the startTransition callback, and where was the other update triggered?

The answer is that when the startTransition method is called, another update is triggered inside React via setState.

React code implementation

// useTransition hook useTransition: function () { ... return mountTransition(); } function mountTransition() {var _mountState2 = mountState(false), const [isPending, setPending] = useState(false); isPending = _mountState2[0], setPending = _mountState2[1]; var start = startTransition.bind(null, setPending); var hook = mountWorkInProgressHook(); hook.memoizedState = start; return [isPending, start]; } function startTransition(setPending, callback) {} function startTransition(setPending, callback) {... setPending(true); SetState prevTransition = ReactCurrentBatchConfig$2.transition; ReactCurrentBatchConfig$2.transition = 1; try { setPending(false); // Call setState callback() the second time; SetState} finally {... }}Copy the code

When we call useTransition hook, react uses useState hook to define isPending and setPending. This isPending is the isPending returned by useTransition.

When startTransition is called, isPending is changed to true by setPending, and isPening is changed to false by setPending, triggering our own defined update in the callback. I’m going to call setState three times in a row, and I’m going to get two React updates. Isn’t that supposed to happen once? React Batch processing mechanism

To explain this, you need to pay special attention to one line of code -reactCurrentBatchConfig $2.transition = 1. This code changes the update context to transition, making setPending(true) different from setPending(false) and callback() contexts.

Here’s a diagram of the process involved in isPending:

For details on workLoop and Lane, see The React series (3).

As you can see from the above figure, setPending(True) context is DiscreteEvent, while setPending(False) and callback() context is Transition. Despite three setStates in a row, two updates are actually required because of the existence of two different types of contexts.

SetPending (True) updates will be processed first. After Fiber Tree coordination, effect processing, and browser rendering, we can see the intermediate state in the page first. After the same process of setPending(false) and callback, we can see the actual result in the page.

(isPending how to work problems solved, ✌🏻).

UseTransition implements a similar effect to Debounce

In the useTransition example, something like throttling -debounce appears when we type in quick succession, as shown below:

After continuous typing, the page displays only intermediate states, and the final list of results is displayed when we stop typing. The whole process looks like a series of inputs that happen to stabilize -debounce and then trigger the React update on the last onchange event.

Does the same logic apply to useTransition?

UseTransition does not have any code for implementing debounce, so how does useTransition implement debounce?

The answer is: high-priority updates interrupt low-priority updates, priority processing.

In the startTransition process, the update triggered by setPending(True) has a higher priority, while the update triggered by setPending(False) and callback has a lower priority. When the update triggered by callback enters the coordination stage, the coordination process can be interrupted and setPending(True) is triggered all the time due to the user’s input, so the update triggered by callback is always interrupted until the user stops input and can be processed completely.

The whole process can be explored through the performance analysis chart.

This is a performance analysis of the example useTransition continuous input. In the figure, we can clearly see that only one long list rendering actually took place. We enlarge the update process of the continuous input on the left as follows:

In this figure, we can clearly see that the entire coordination process of callback updates is interruptible, interrupted by setPending(True) updates triggered by successive inputs. Let’s zoom in on the middle of the image as follows:

In this figure, setPending(True) updates interrupt callback updates at a glance.

In summary, through the three performance analysis graphs, it can be found that useTransition achieves similar debounce effect because high-priority updates interrupt low-priority updates, making low-priority updates delay processing.

(useTransition implements problems similar to Debounce effects, ✌🏻).

UseTransition implements throtte effects

In the example useTransition, if you keep typing, 5s Max, the long list will inevitably render, just like the shaker-throtte effect.

This is because react defines a timeout for each type of update, in order to prevent low-priority updates from being interrupted by high-priority updates and not being processed. If the update is not processed within the timeout time, then the priority of the update will be raised to the highest ImmediatePriority, which takes precedence.

The transiton update priority is NormalPriority, and timeout is 5000ms, that is, 5s. If the transition update goes beyond 5s and is not processed because it has been interrupted by higher-priority updates, its priority will be elevated to its ImmediatePriority. You’re throttle.

(useTransition implement problems solving similar to Throtte effects, ✌🏻)

Difference between using useTransition and startTransition

In the startTransiton and useTransition examples, an interesting observation is that using useTransition will produce debounce effects when the user is typing continuously, whereas using startTransition directly will not.

Why is that?

StartTransition (startTransition)

function startTransition(scope) { var prevTransition = ReactCurrentBatchConfig.transition; ReactCurrentBatchConfig.transition = 1; // Modify the update context try {scope(); // Trigger update} finally {... }}Copy the code

By contrast with useTransition startTransition, we can find that setPending(True) is missing in startTransition. It is this missing setPending(true) that causes direct use of startTransition to have no Debounce effect.

There is a key point to declare here. That is, in Concurrent mode, when low-priority updates are interrupted by high-priority updates, the coordination that started low-priority updates is cleaned up and the low-priority updates are reset to an inactive state.

When using useTransition, the transition update is continuously interrupted by setPending(true), and is reset to unstarted each time. As a result, transition updates can only be processed effectively if the user stops typing (or exceeds 5s), resulting in a debounce effect.

When startTransition is used directly, continuous input does not reset the transition update because there is no setPending(true) interrupt, although the coordination is interrupted every 5ms. When the Transition update completes coordination, the browser rendering process naturally begins without the Debounce effect.

(resolve using useTransition vs. using startTransition directly, ✌🏻).

Transition Updates

Finally, we summarize the process of transition:

This is a nice picture of how transition works.

(Transition update processing problems resolved, ✌🏻).

Write in the last

This is the end of the article. Let’s make a summary and see what we can learn from this article:

  • React18 provides a new API – useTransition and startTransition to help us achieve update coordination interruptible, can greatly improve the user experience;
  • The transition update can be interrupted by high-priority updates, and then reset to an unstarted state.
  • If the transition update is interrupted, it will be processed at the latest 5s.
  • Use useTransition in function components and startTransition directly in class components.
  • Different update contexts lead to different update coordination modes.

portal

  • React series 1: How react works

  • React series (2) : Use binary tree middle order traversal to understand the coordination process of fiber tree

  • React Series 3: Understand Concurrent mode through loop 3

Reference documentation

  • Introduction to Concurrent mode (experimental)

  • startTransition