In a recent update to PR #21488, Brian Vaughn made changes to some of the React apis and internal flags.

The most notable change is the addition of the createRoot API to the React entry.

This change has been interpreted by the industry as a sign that Concurrent Mode (HEREINAFTER referred to as CM) will soon be stable and present in the official release.

Act17 is a transitional release to stabilize CM. Once CM is stable, the progress of V18 will be much faster.

It can be said that from 18th to 21st, the React team mainly focused on CM. Then:

  • What is CM?

  • What problem does CM solve with React?

  • Why is CM still unstable after almost 4 years, spanning versions 16 and 17?

This article will answer the question.

What is the CM

To understand what CM (concurrent mode) is, you need to know the React source code workflow.

React can be divided into two phases:

  • renderphase

The render phase calculates what changes in an update (via the diff algorithm), so named because the render function of the component is called in this phase.

The Render phase can be asynchronous (depending on the scenario that triggers the update).

  • commitphase

During the Commit phase, the parts calculated in the Render phase that need to be changed are rendered in the view. In the case of ReactDOM, appendChild, removeChild, etc.

The COMMIT phase must be a synchronous call (so the user doesn’t see an incomplete rendered UI)

The application we created with reactdom.render is legacy mode.

In this mode, the next Render phase corresponds to a commit phase.

If the application we create through reactdom.createroot (API not available in the current stable version) is CM (Concurrent mode) mentioned in the beginning.

Under CM, updates have the concept of priority, and the RENDER phase can be interrupted by high-priority updates.

So the Render phase may be repeated several times (interrupted and restarted).

Maybe multiple Render phases correspond to a commit phase.

In addition, there is a blocking mode for developers to slowly transition from Legacy mode to CM.

You can see the features supported by different modes from the feature comparison:

Why CM?

If you know what CM is, what good is he? Why did the React core team take more than 3 years to implement it?

It starts with React’s design philosophy.

React design philosophy:

We believe React is the preferred way to build large, responsive Web applications in JavaScript.

Rapid response is the key.

So what affects fast response? Answer from the React team:

CPU bottlenecks and IO bottlenecks

The bottleneck of the CPU

Consider the following demo, where we render a list item for 3000:

function App() {
  const len = 3000;
  return (
    <ul>
      {Array(len).fill(0).map((_, i) => <li>{i}</li>)}
    </ul>
  );
}

const rootEl = document.querySelector("#root");
ReactDOM.render(<App/>, rootEl);  
Copy the code

As mentioned earlier, in Legacy mode the Render phase is not interrupted, so all 3000 Li render tasks have to be done in the same browser macro task.

Long computations can block threads and cause pages to drop frames, which is a CPU bottleneck.

The solution is to enable CM, make render phase interruptible,

Give control to the browser when the browser has little time left in a frame. Wait for the spare time of the next frame before continuing with the render component.

The bottleneck of the IO

In addition to the delay caused by long computing time, the loading state of network requests also causes the page cannot interact, which is the BOTTLENECK of I/O.

IO bottlenecks exist.

As a front-end, all you can do is request the data you need as early as possible.

More often than not, however, code maintainability is at odds with request efficiency.

What does that mean? Here’s an example:

Suppose we encapsulate the method useFetch that requests data, distinguishing whether or not the data is requested by the presence of the return value.

function App() {
  const data = useFetch();
  
  return {data ? <User data={data}/> : null};
}
Copy the code

To improve code maintainability, useFetch resides in the same component App as User, the component to be rendered.

However, what if there is a further request for data within the User component (the profile data below)?

function User({data}) {
  const{id, name} = data? .id || {};const profile = useFetch(id);
  
  return (
    <div>
      <p>{name}</p>
      {profile ? <Profile data={profile} /> : null}
    </div>)}Copy the code

In line with code maintainability, useFetch resides in the same component User as the component Profile to render.

However, by organizing the code this way, the Profile component can only wait for User render to render.

Data can only flow down one layer at a time, like water in a waterfall.

This inefficient method of requesting data is known as waterfall.

In order to improve the efficiency of the request, we can include “operation requesting data required by Profile component” in App component and merge it in useFetch:

function App() {
  const data = useFetch();
  
  return {data ? <User data={data}/> : null};
}
Copy the code

But this reduces code maintainability (the Profile component is too far from the Profile data).

The React team drew lessons from the Relay team and proposed Server Components with help of Suspense features.

The goal is to balance code maintainability with request efficiency when dealing with IO bottlenecks.

Implementation of this feature requires different priorities for updates in CM.

Why is CM taking so long?

Next, let’s take a bottom-up look at how difficult it has been to get CM across in terms of source code, features, and ecology.

The source code level

Priority algorithm modification

React implemented basic CM functionality prior to V16.13.

As we talked before, CM has the concept of updating priorities. Previously the expirationTime of updates is marked by a millisecond expirationTime.

  • Determine the priority by comparing different expirationTime updates

  • By comparing the updated expirationTime with the current time to determine whether the update is expired (expiration needs to be executed synchronously)

However, as a time-dependent floating point number, expirationTime cannot represent the concept of a batch priority.

To implement the higher-level Server Components features, you need the concept of batch priorities.

As a result, Andrew Clark, a key member of the team, began a long process of prioritizing algorithms, see: PR Lanes

Offscreen support

At the same time, Luna Ruan, another member, was working on a new API, Offscreen.

This is the Keep-alive feature of React.

Subscribe to external sources

Before starting CM, the following three life cycles will be called only once in an update:

  • componentWillMount

  • componentWillReceiveProps

  • componentWillUpdate

However, once CM is turned on, they can be called multiple times because the Render phase can be interrupted and repeated.

When subscribes to an external source (such as a registered event callback), updates may not be timely or memory leaks may occur.

For example: bindEvent is a publish-subscription-based external dependency (such as a native DOM event) :

class App {
  componentWillMount() {
    bindEvent('eventA'.data= > {
      thie.setState({data});
    });
  }
  componentWillUnmount() {
    bindEvent('eventA');
  }
  render() {
    return <Card data={this.state.data}/>; }}Copy the code

Bind in componentWillMount, unbind in componentWillUnmount.

When an event is received, the data is updated.

When the Render phase repeatedly breaks and pauses, it is possible to:

Before the event is finally bound (before bindEvent is executed), the event source fires the event

At this point, the App component has not registered the event (bindEvent has not been executed), so the data obtained by the App is old.

To address this potential problem, core member Brian Vaughn developed a feature called create-Subscription

Used to regulate subscriptions and updates to external sources in React.

Simply put, the registration and update of external sources are tied to the component’s status update mechanism during the COMMIT phase.

Feature level

When support at the source level is complete, the development of new features based on CM is on the agenda.

Suspense is Suspense.

[Umbrella] Releasing Suspense #13206. This PR keeps track of Suspense #13206.

The Umbrella tag means that this PR affects many libraries, components, and tools

As you can see, it’s a long time line from 18 years to the last few days.

At first Suspense was a front-end feature, and React SSR could only pass string data forward.

React implemented a component streaming protocol for SSR that streams components, not just HTML strings.

Suspense is given more responsibility at this point. It also has more complex priorities, which is a big reason for the prioritization algorithm that I just talked about.

The result was the Server Components concept, introduced earlier this year.

Ecological level

When the source level is supported and features are developed, will it be seamless?

Still early.

As an eight-year-old ship, React has a huge amount of work to do in between upgrades and eventual community adoption.

To help the community slowly transition to CM, React does the following:

  • Develop the ScrictMode feature, which is enabled by default, and standardize developer writing

  • Marking componentWillXXX as unsafe reminds users not to use it, it will be deprecated in the future

  • A new life cycle (getDerivedStateFromProps, getSnapshotBeforeUpdate) is proposed to replace the deprecated life cycle such as general

  • A blocking mode is developed between Legacy mode and CM mode

And that’s just the easiest part of the transition.

The hard part:

How will the community’s current accumulation of legacy based libraries be migrated?

Many animation libraries and state management libraries (such as mobX) are not easy to migrate.

conclusion

We introduced CM’s story and the difficulties of his migration.

In this article, you know how difficult it was to add createRoot to React.

Fortunately, it was all worth it. The barriers to React were early open source and large community.

So starting with CM, React is probably the most complex view framework in the front-end domain.

There won’t be any react-like framework that implements the same feature as React.

But some people say that CM brings these functions are chicken ribs, I don’t need them.

What do you think of CM? Welcome to leave your comments.