preface

This article is available on Github: github.com/beichensky/… Welcome Star, welcome Follow!

React 18 features 1: Automatic batch updates. In the React release, updates are prioritized.

So what do you do if you want low-priority updates? That’s the topic of today: gradual updates.

If you don’t know how to set up an experience environment for React18, check out this article: Using Vite to test React18

StartTransition: Update with a gradient

  • StartTransition accepts a callback function that postpones setState updates put into it

  • Allows components to delay rendering of slower updates so that more important updates can be rendered immediately

For example

Let’s take a look at an example. When you use Google or Baidu to search, you will encounter the following scenario:

The presentation here is divided into two parts

  • Part of it is the search in the input box

  • The other part is the associative content of the presentation.

Analysis from the perspective of users:

  • The content in the input box needs to be updated instantly

  • And the content of the association is required to request or load, even at the beginning of the association is not accurate, not used. So the user can accept some delay in this part of the content.

In this case, the user’s input is a high-priority operation, and the change in the associative region is a low-priority operation.

Mock code to implement this example

Let’s write some code to implement this search box.

App.jsx

import React, { useEffect, useState, startTransition } from 'react';
import ReactDOM from 'react-dom';

const App = () = > {
    const [value, setValue] = useState(' ');
    const [keywords, setKeywords] = useState([]);

    useEffect(() = > {
        const getList = () = > {
            const list = value
                ? Array.from({ length: 10000 }, (_, index) = > ({
                      id: index,
                      keyword: `${value} -- ${index}`,})) : [];return Promise.resolve(list);
        };
        getList().then(res= > setKeywords(res));
    }, [value]);

    return (
        <>
            <input value={value} onChange={e= > setValue(e.target.value)} />
            <ul>
                {keywords.map(({ id, keyword }) => (
                    <li key={id}>{keyword}</li>
                ))}
            </ul>
        </>
    );
};

// Use react 18's new concurrency mode for DOM render
ReactDOM.createRoot(document.getElementById('root')).render(<App />);

// Legacy old mode
// ReactDOM.render(<App />, document.getElementById('root')!)
Copy the code

Then let’s take a look at the current effect (we won’t discuss anti-shake or throttling here) :

It can be seen that not only the content of the association area is slow to load, but also the user’s interaction content is slow to respond.

Now that we are talking about low-priority updates, can we make the content of the associative area low-priority updates to avoid preempting user action updates?

Next, the hero steps in and changes the code using startTransition.

Enable gradient update

App.jsx

import React, { useEffect, useState, startTransition } from 'react';
import ReactDOM from 'react-dom';

const App = () = > {
    const [value, setValue] = useState(' ');
    const [keywords, setKeywords] = useState([]);

    useEffect(() = > {
        const getList = () = > {
            const list = value
                ? Array.from({ length: 10000 }, (_, index) = > ({
                      id: index,
                      keyword: `${value} -- ${index}`,})) : [];return Promise.resolve(list);
        };
-        //getList().then(res => setKeywords(res));
        // Just wrap setKeywords in startTransition to enable gradual updating
+        getList().then(res= > startTransition(() = > setKeywords(res)));
    }, [value]);

    return (
        <>
            <input value={value} onChange={e= > setValue(e.target.value)} />
            <ul>
                {keywords.map(({ id, keyword }) => (
                    <li key={id}>{keyword}</li>
                ))}
            </ul>
        </>
    );
};

// Use react 18's new concurrency mode for DOM render
ReactDOM.createRoot(document.getElementById('root')).render(<App />);

// Legacy old mode
// ReactDOM.render(<App />, document.getElementById('root')!)
Copy the code

Re-execute and see what happens:

As you can see, the interface is much more responsive than before.

UseDeferredValue: Returns the value of a deferred response

UseDeferredValue is equivalent to the syntax sugar of startTransition(() => setState(XXX)). SetState is called internally, but this update has a lower priority

So let’s rewrite the above code with useDeferredValue to see if anything is different.

App.jsx

import React, { useEffect, useState, useDeferredValue } from 'react';
import ReactDOM from 'react-dom';

const App = () = > {
    const [value, setValue] = useState(' ');
    const [keywords, setKeywords] = useState([]);
+    const text = useDeferredValue(value);

    useEffect(() = > {
        const getList = () = > {
            const list = value
                ? Array.from({ length: 10000 }, (_, index) = > ({
                      id: index,
                      keyword: `${value} -- ${index}`,})) : [];return Promise.resolve(list);
        };
        getList().then(res= > setKeywords(res));
        // Just update the dependent value from value to text
+    }, [text]);

    return (
        <>
            <input value={value} onChange={e= > setValue(e.target.value)} />
            <ul>
                {keywords.map(({ id, keyword }) => (
                    <li key={id}>{keyword}</li>
                ))}
            </ul>
        </>
    );
};

// Use react 18's new concurrency mode for DOM render
ReactDOM.createRoot(document.getElementById('root')).render(<App />);

// Legacy old mode
// ReactDOM.render(<App />, document.getElementById('root')!)
Copy the code

Take a look at the interface response speed at this point:

You can see that the response time is about the same as when you use startTransition.

Third, useTransition

Suspense & SuspenseList remember the Suspense and User components you used in Suspense 18? We will show the usage and features of useTransition based on these two components.

Take an example of asynchronous loading

Let’s say we currently need to wrap up the User component with Suspense, where there are time-consuming operations such as network requests inside the User component. Clicking the button triggers an update of the User component to retrieve the data again

App.jsx

import React, { Suspense, useState } from 'react';
import ReactDOM from 'react-dom';

// Encapsulate the promise
function wrapPromise(promise) {
    let status = 'pending';
    let result;
    let suspender = promise.then(
        r= > {
            status = 'success';
            result = r;
        },
        e= > {
            status = 'error'; result = e; });return {
        read() {
            if (status === 'pending') {
                throw suspender;
            } else if (status === 'error') {
                throw result;
            } else if (status === 'success') {
                returnresult; }}}; }// Network request, get user data
const requestUser = id= >
    new Promise(resolve= >
        setTimeout(() = > resolve({ id, name: ` user${id}`.age: 10 + id }), id * 100));/ / the User component
const User = props= > {
    const user = props.resource.read();
    return <div>The current user is {user.name}.</div>;
};

// Obtain the corresponding resource by id
const getResource = id= > wrapPromise(requestUser(id));

const App = () = > {
    const [resource, setResource] = useState(getResource(10));

    return (
        <>
            <Suspense fallback={<div>Loading...</div>} ><User resource={resource} />
            </Suspense>
            <button onClick={()= >SetResource (wrapPromise(requestUser(1))}> Switch the user</button>
        </>
    );
};

// Use react 18's new concurrency mode for DOM render
ReactDOM.createRoot(document.getElementById('root')).render(<App />);

// Legacy old mode
// ReactDOM.render(<App />, document.getElementById('root')!)
Copy the code

OK, let’s have a look at the effect:

As you can see, loading effect will appear when loading for the first time, which is normal, but when you click the button to switch users, loading effect will still appear. This is not a problem originally, but when the request speed is very fast, there will be a problem of blinking. In this case, loading is not necessary.

That’s where the useTransition comes in handy.

concept

  • UseTransition allows components to wait for content to load before switching to the next screen to avoid unnecessary loading

  • Allows components to defer slower data acquisition updates to later renders (low-priority updates) so that more important updates can be rendered immediately

  • UseTransition returns an array of two elements:

    • isPending: BooleanNotify us if we are waiting for the transition effect to complete
    • startTransition: FunctionIt is used to wrap the state that needs to be updated later

Use useTransition to modify the above example

Using the startTransition returned by useTransition to wrap the setState that needs to be updated lowers the priority of the update and buffers the interface until the next interface is ready to be updated.

App.jsx

import React, { Suspense, useState, useTransition } from 'react';
import ReactDOM from 'react-dom';

// Encapsulate the promise
function wrapPromise(promise) {
    let status = 'pending';
    let result;
    let suspender = promise.then(
        r= > {
            status = 'success';
            result = r;
        },
        e= > {
            status = 'error'; result = e; });return {
        read() {
            if (status === 'pending') {
                throw suspender;
            } else if (status === 'error') {
                throw result;
            } else if (status === 'success') {
                returnresult; }}}; }// Network request, get user data
const requestUser = id= >
    new Promise(resolve= >
        setTimeout(() = > resolve({ id, name: ` user${id}`.age: 10 + id }), id * 100));/ / the User component
const User = props= > {
    const user = props.resource.read();
    return <div>The current user is {user.name}.</div>;
};

// Obtain the corresponding resource by id
const getResource = id= > wrapPromise(requestUser(id));

const App = () = > {
    const [resource, setResource] = useState(getResource(10));
+    const [isPending, startTransition] = useTransition();

    return (
        <>
            <Suspense fallback={<div>Loading...</div>} ><User resource={resource} />
            </Suspense>
+            <button onClick={()= >StartTransition (() => setResource(wrapPromise(requestUser(1))))}> Switch users</button>
        </>
    );
};

// Use react 18's new concurrency mode for DOM render
ReactDOM.createRoot(document.getElementById('root')).render(<App />);

// Legacy old mode
// ReactDOM.render(<App />, document.getElementById('root')!)
Copy the code

As you can see, the loading state does not appear, and the flash state disappears:

The problem is that if a time-consuming operation takes a long time, there will be no feedback from the user if it is not loaded.

Don’t worry, the first element isPending is now available:

App.jsx

import React, { Suspense, useState, useTransition } from 'react';
import ReactDOM from 'react-dom';

// Encapsulate the promise
function wrapPromise(promise) {
    let status = 'pending';
    let result;
    let suspender = promise.then(
        r= > {
            status = 'success';
            result = r;
        },
        e= > {
            status = 'error'; result = e; });return {
        read() {
            if (status === 'pending') {
                throw suspender;
            } else if (status === 'error') {
                throw result;
            } else if (status === 'success') {
                returnresult; }}}; }// Network request, get user data
const requestUser = id= >
    new Promise(resolve= >
        setTimeout(() = > resolve({ id, name: ` user${id}`.age: 10 + id }), id * 100));/ / the User component
const User = props= > {
    const user = props.resource.read();
    return <div>The current user is {user.name}.</div>;
};

// Obtain the corresponding resource by id
const getResource = id= > wrapPromise(requestUser(id));

const App = () = > {
    const [resource, setResource] = useState(getResource(10));
    const [isPending, startTransition] = useTransition();

    return (
        <>
            <Suspense fallback={<div>Loading...</div>} ><User resource={resource} />
            </Suspense>
+            {isPending ? <div>Loading</div> : null}
            <button
                onClick={()= >StartTransition (() => setResource(wrapPromise(requestUser(20))))} > Switch users</button>
        </>
    );
};

// Use react 18's new concurrency mode for DOM render
ReactDOM.createRoot(document.getElementById('root')).render(<App />);

// Legacy old mode
// ReactDOM.render(<App />, document.getElementById('root')!)
Copy the code

At this time, click the button to switch the user, and there will be a waiting time of about 2s. Then the loading state can be displayed to remind the user:

Therefore, when using useTransition, it is important to pay attention to the following scenarios:

  • You can use startTransition directly in the return value when you know that the time-consuming operation is extremely fast

  • If the response speed cannot be guaranteed, isPending is still needed to judge and display the transition state

  • If you have high requirements on update priorities, do not use useTransition

A link to the

  • React 18 with Vite

  • React 18 New feature 1: Automatic batch update

  • Suspense & SuspenseList

Afterword.

All right, we’ve covered the usage and scenarios of startTransition, useDeferredValue, and useTransition.

All the code has been posted in this article.

If there is something wrong or not precise in the article, you are welcome to put forward your valuable opinions. Thank you very much.

If you like or help, welcome Star.