This article was first published on my personal blog: teobler.com

Since the inception of the three frameworks, each framework has made a lot of efforts to improve the efficiency of developers, but it seems that technological innovation has reached a bottleneck. With the exception of React, which introduced the idea of a functional approach, it doesn’t feel as amazing as it did when it moved from the DOM era to the data-driven era. React focuses on the user experience, allowing developers to improve the user experience through the framework itself without too much effort.

In recent releases, React introduced a new feature called Concurrent Mode, as well as Suspense to improve the user experience. The React app gets progressively more congested as it gets bigger. The new feature is intended to address these issues from the beginning.

While these features are still in the experimental stage, the React team doesn’t recommend using them in production, but most of the features are already in place, and they’re already being used in new website features, so we can play around with a relatively mature technology. So let me show you what this is.

Concurrent Mode

WHAT

So what is Concurrent Mode?

Concurrent Mode is a set of new features that help React apps stay responsive and causeway to the user’s device capabilities and network speed.

As explained on the website, Concurrent Mode includes a number of new features that can respond to different device performance and network speeds to provide the best user experience for different devices and network speeds. So the question is, how does it do that?

HOW

(Interruptible rendering)

React rendering cannot be interrupted during render (creating new DOM nodes, etc.), and the rendering process will hog up the JS thread, causing the browser to fail to respond to the user’s rendering in real time, resulting in a feeling of gridlock.

In Concurrent Mode, rendering can be interrupted, which means React can release the main thread to the browser for more urgent user operations.

Imagine a common scenario: the user to retrieve some information in an input box, the text input box will change after the page to apply colours to a drawing to show the latest results, but you will find that every input caton, because every time to render will block the main thread, the browser will have no way of corresponding user input in the input box. Of course the common solutions these days are debouncing or throtting. However, there are some problems in this way. First, there is no way for the page to reflect the user’s input in real time. The user will find that the page is refreshed only once when many characters are input, and it will not be updated in real time. The second problem is that lag still occurs on devices with poor performance.

If a user enters a new character while render is in progress, React can pause Render to allow the browser to update the user’s input first. React will render the latest page in memory, wait until the first render is complete, then update the latest page directly. Ensure that users can see the latest page.

The main branch is visible to the user and can be suspended. React creates a new branch to do the latest rendering. When the main branch is rendered, the new branch is merged to get the latest view.

Optional Loading Sequences (Intentional Loading Sequences)

In order to improve the user experience, we often add skeleton to the new page when jumping to the page. This is to prevent the user from seeing a blank page without getting the data to render.

In Concurrent Mode, React can stay on the first page for a while, at which time React will render a new page with the received data in memory, and then jump directly to a finished page after the page rendering is complete. This behavior is more intuitive to users. It should also be noted that any user action can be captured during the first page wait, meaning that no user action is blocked during the wait time.

conclusion

To sum up, the new Concurrent Mode allows React to perform parallel processing in different states at the same time, merging all changes after the Concurrent state ends. This feature focuses on two main points:

  • For cpus (such as DOM node creation), this parallelism means that higher-priority updates can interrupt rendering
  • In the case of IO (such as getting data from the server), this parallelism means React can use some of the data it gets first to build the DOM in memory and render it all in one go without affecting the current page

For developers, React usage hasn’t changed much. How you wrote React in the past and how you’ll write React in the future won’t give developers a sense of disconnect. Here are a few examples to see how it works.

Suspense

Preparation before starting

Unlike the previous features, Concurrent Mode needs to be turned on manually by the developer (Suspense doesn’t seem to work, but I’ll do it now for the code in the next article). For tOU’s sake, we create a new project with CRA. To use Concurrent Mode, we need to make the following changes:

  • To delete the React version of the project, use the experimental NPM install react@experimental react-dom@experimental

  • Add the experimental React type reference to the react-app-env.d.ts file for normal use of TypeScript

    /// <reference types="react-dom/experimental" />
    /// <reference types="react/experimental" />
    Copy the code
  • Enable Concurrent Mode in index. TSX

    ReactDOM.unstable_createRoot(
      document.getElementById("root") as HTMLElement
    ).render(<App />);
    Copy the code
  • In Suspense if you need to “suspend” your components when fetching back-end data, you need a “Promise Wrapper” implemented as React requires, in this case I chose SWR

    • According to the following results, the current SWR forSuspenseThe implementation is not yet complete, but there is a PR in place

All of the code in this article can be found on my Github repo. If you have enough time, clone a copy and eat it with the article.

Data Fetching

Suspense the React team has added a new component in 16.6, Suspense is more of a mechanic than a component. This component can be “suspended” while the child is rendering, rendering a component with a waiting icon that you set up, and then displaying the child after the child is rendered. Prior to Concurrent Mode, this component was usually used for lazy loading:

const ProfilePage = React.lazy(() => import('./ProfilePage')); // Lazy-loaded

// Show a spinner while the profile is loading
<Suspense fallback={<Spinner />}>
  <ProfilePage />
</Suspense>
Copy the code

In the latest React release, Suspense components can now be used to “suspend” anything, such as a component such as an image, a script, etc., when fetching data from the server. We’ll just take data from the back end as an example here.

In traditional data acquisition, components are often rendered first and then retrieved from the back end. After retrieving the data, the components are rerendered and the data is Fetch-on-render to the components. There are a few problems with this, most notably triggering a waterfall — the first render component triggers a network request, and then the second Render component triggers a network request, but the two requests can be processed concurrently.

Then there might be some tricks to avoid this, such as I send two requests before the render components, and when both requests are finished I Fetch the data to the Render component. This does solve the waterfall problem, but introduces a new problem — if the first request takes 2s and the second request only takes 500ms, the second request, even if it has the data, must wait for the render component to complete after the first request.

Suspense solves this problem by taking the render-as-you-fetch approach. Suspense initiates requests first and begins component rendering almost at the same time as the request is sent, without waiting for the result of the request to be returned. Instead of a Promise, the component gets a special data structure provided by the “Promise Wrapper “library of your choice (as mentioned above, I used SWR in my case). React “suspends” this component because the required data is not ready, and continues rendering other components. After the others have finished rendering React renders fallbacks of components closest to Suspense components. After a request is successful, the corresponding component is rerendered.

In my example I try to do it in Suspense + SWR + Axios.

Fetch data in parent

In the first version I tried to fetch data in the parent component (PageProfile) and render it in the child component:

const App: React.FC = () => ( <Suspense fallback={<h1>Loading... </h1>}> <PageProfile /> </Suspense> ); export default App;Copy the code
export const PageProfile: React.FC = () => {
  const [id, setId] = useState(0);
  const { user, postList } = useData(id);

  return (
    <>
      <button onClick={() => setId(id + 1)}>next</button>
      <Profile user={user} />
      <ProfileTimeline postList={postList} />
    </>
  );
};
Copy the code
export const useData = (id: number) = > {
  const { data: user } = useRequest(
    { baseURL: BASE_API, url: `/api/fake-user/${id}`, method: "get" },
    {
      suspense: true,});const { data: postList } = useRequest(
    { baseURL: BASE_API, url: `/api/fake-list/${id}`, method: "get" },
    {
      suspense: true,});return {
    user,
    postList,
  };
};
Copy the code
import useSWR, { ConfigInterface, responseInterface } from "swr";
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";

export type GetRequest = AxiosRequestConfig | null;

interface Return<Data, Error>
  extends Pick<responseInterface<AxiosResponse<Data>, AxiosError<Error>>, "isValidating" | "revalidate" | "error"> {
  data: Data | undefined;
  response: AxiosResponse<Data> | undefined;
}

export interface Config<Data = unknown, Error = unknown>
  extends Omit<ConfigInterface<AxiosResponse<Data>, AxiosError<Error>>, "initialData"> {}

export const useRequest = <Data = unknown, Error= unknown>( requestConfig: GetRequest, { ... config }: Config<Data,Error> = {},
): Return<Data, Error> = > {const { data: response, error, isValidating, revalidate } = useSWR<AxiosResponse<Data>, AxiosError<Error>>(
    requestConfig && JSON.stringify(requestConfig),
    (a)= >axios(requestConfig!) , {... config, }, );return {
    data: response && response.data,
    response,
    error,
    isValidating,
    revalidate,
  };
};
Copy the code

But I don’t know if there is something wrong with my writing style. There are a few questions THAT I still don’t understand:

  • In order to achieve therender-as-you-fetchIt is mentioned in the document that data can be fetched as early as possible, so that render and FETCH can be parallel and the time to get data can be shortened (if I understand correctly).
    • My idea is to fetch data in the parent component first and then use twoSuspenseProfileThe component andProfileTimelineThe components are wrapped, and then you can render the corresponding components after getting the corresponding data (user and postList)
    • However, in the process of using it, I found that “fetch data in which component, must be usedSuspenseWrap this component, otherwise it will report an error “, so here I will put the wholePageProfileWrapped it up. And at this point even I use twoSuspenseProfileThe component andProfileTimelineThe loading information is only displayed in the outermost layer of the component, so there is no way to implement thisrender-as-you-fetch
    • SWR will have a random request written in this way, the reason is not found yet
      • The first second request in the figure is the requested user and postList data, respectively, but the user is requested again after it is done
  • SWR is not currently implemented inSuspenseIn waterfall mode, avoid waterfall, so two requests are sent sequentially and the waiting time is the sum, but check github already has itprNow that we’ve solved this problem, we’re in the CodereView phase

Fetch data in the current component

In order to solve the above problem, I wrote it differently:

const App: React.FC = () => { const [id, setId] = useState(0); return ( <> <button onClick={() => setId(id + 1)}>next</button> <Suspense fallback={<h1>Loading profile... </h1>}> <Profile id={id} /> <Suspense fallback={<h1>Loading posts... </h1>}> <ProfileTimeline id={id} /> </Suspense> </Suspense> </> ); }; export default AppCopy the code
export const Profile: React.FC<{ id: number }> = ({ id }) => {
  const { data: user } = useRequest({ baseURL: BASE_API, url: `/api/fake-user/${id}`, method: "get" }, { suspense: true });

  return <h1>{user.name}</h1>;
};
Copy the code
export const ProfileTimeline: React.FC<{ id: number }> = ({ id }) => {
  const { data: postList } = useRequest(
    { baseURL: BASE_API, url: `/api/fake-list/${id}`, method: "get" },
    { suspense: true },
  );

  return (
    <ul>
      {postList.data.map((listData: { id: number; text: string }) => (
        <li key={listData.id}>{listData.text}</li>
      ))}
    </ul>
  );
};
Copy the code

At this time, I put the corresponding request into the sub-component. This writing is normal no matter the loading state of two components or the network request. However, according to my understanding, this writing is not in line with the original intention of React. In the document React advocates kicking off the network request at the top level (such as the upper-layer component) and then rendering the component regardless of the result:

const resource = fetchProfileData(); function ProfilePage() { return ( <Suspense fallback={<h1>Loading profile... </h1>}> <ProfileDetails /> <Suspense fallback={<h1>Loading posts... </h1>}> <ProfileTimeline /> </Suspense> </Suspense> ); } function ProfileDetails() { const user = resource.user.read(); return <h1>{user.name}</h1>; } function ProfileTimeline() { const posts = resource.posts.read(); return ( <ul> {posts.map(post => ( <li key={post.id}>{post.text}</li> ))} </ul> ); }Copy the code

But the current way of writing it is clearly a network request made when the child component is being rendered. As for this issue, I will wait for SWR Merge to complete the latest PR update and the next version before conducting the experiment.

one more thing

Suspense has another benefit for developers beyond what has been explained above — you don’t have to write Race conditions anymore.

In the previous request, you didn’t get any data when you first rendered the component, so you needed to write a judgment like this:

if(requestStage ! == RequestStage.SUCCESS)return null;
Copy the code

And in the same project, with different hands, there are judgments like this:

if (requestStage === RequestStage.START) return null;
Copy the code

And if the request hangs, you still have to:

return requestStage === RequestStage.FAILED ? (
  <SomeComponentYouWantToShow />
) : (
  <YourComponent />
);
Copy the code

In Suspense, you don’t need to write these things any more. In Suspense, data will be directly rendered in Suspense fallback. As for request errors, you just need to add an error boundary in the outer layer. I don’t want to expand on that here, see the documentation.

Generally speaking, Suspense has good intention and can improve user experience. Maybe all tools including React are still in the experimental stage and there are still some problems. I will try to find out how to solve these problems and then come back to update. I’ll continue with the UI section in the next post.

Welcome to follow my public number