Written by Arjuna Sky Kok ✏️
Part of React 18’s experimental Concurrent Mode is a new feature called startTransition
, which prevents an expensive UI render from being executed immediately.
To understand why we need this feature, remember that forcing expensive UI renders to be done immediately can block lighter and more urgent UI renders from rendering in time. This can frustrate users who need immediate response from the urgent UI renders.
An example of an urgent UI render would be typing in a search bar. When you type, you want to see your typing manifested and begin searching immediately. If the app freezes and the searching stops, you get frustrated. Other expensive UI renders can bog down the whole app, including your light UI renders that are supposed to be fast (like seeing search results as you type).
When developing your React app, you can avoid this problem by debouncing or throttling. Unfortunately, using debouncing or throttling can still cause an app to become unresponsive.
startTransition
allows you to mark certain updates in the app as non-urgent, so they are paused while the more urgent updates are prioritized. This makes your app feel faster, and can reduce the burden of rendering items in your app that are not strictly necessary. Therefore, no matter what you are rendering, your app is still responding to your user’s input.
In this article, we’ll learn how to use startTransition
in your React app in order to delay the non-urgent UI updates to avoid blocking urgent UI updates. With this feature, you can convert your slow React app into a responsive one in no time.
Before we begin, note that React 18 is still in alpha at the time of writing, so startTransition
is not yet part of a stable release.
Getting started with React 18
Before beginning the tutorial, ensure you have the following:
- Working knowledge of React
- Node.js installed on your machine
Let’s begin by creating a React project with create-react-app:
$ npx create-react-app starttransition_demo
The command above created a React project using the latest stable version of React, which is version 17. We need to use React 18. Go inside the project directory and remove the node_modules
directory:
$ cd starttransition_demo/
$ rm -rf node_modules
On Windows, you have to use a different command to remove the directory. After removing the directory, edit package.json
. Find these lines:
"react": "^17.0.2",
"react-dom": "^17.0.2",
Then, change the version of React from 17 to alpha:
"react": "alpha",
"react-dom": "alpha",
Finally, install the libraries with yarn
:
$ yarn install
To make sure that you have React 18 installed, you can check it from the node_modules
directory like so:
$ grep version node_modules/react/package.json
"version": "18.0.0-alpha-6ecad79cc-20211006",
On Windows, you can open the file directly.
Run the server to make sure you can run the React 18 app:
yarn start
Open http://localhost:3000 in your browser. You should see the familiar default page of a React project with a rotating React logo.
Enabling Concurrent Mode
By default, our React project doesn’t support Concurrent Mode. We need to enable it by rendering the root React node in a different way.
Open src/index.js
. You can see that we render the root node with the render
static method from ReactDOM
:
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
To enable Concurrent Mode, we need to create the root node first then use the render
method from that instance. Change the lines above to the lines below:
const container = document.getElementById('root')
const root = ReactDOM.createRoot(container);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Notice the createRoot
method from ReactDOM
. This will create a root node.
Setting up a testing environment
First, let’s create a React app with a light UI render and an expensive UI render. Open src/App.js
. You can see the App
function definition displaying a React logo, a p
tag, and a link.
Replace the App
function with the code below:
function App() {
const [search_text, setSearchText] = useState("");
const [search_result, setSearchResult] = useState();
const handleChange = e => {
setSearchText(e.target.value);
};
useEffect(() => {
if (search_text==="") {
setSearchResult(null);
} else {
const rows = Array.from(Array(5000), (_, index) => {
return (
<div key={index}>
<img src={logo} className="App-logo" alt="logo" />
<div>{index + 1}. {search_text}</div>
</div>
);
});
const list = <div>{rows}</div>;
setSearchResult(list);
}
}, [search_text]);
return (
<div className="App">
<header className="App-header">
<div className="SearchEngine">
<div className="SearchInput">
<input type="text" value={search_text} onChange={handleChange} />
</div>
<div className="SearchResult">
{search_result}
</div>
</div>
</header>
</div>
);
}
You need to import useEffect
and useState
. Put this line on top of the file:
import {useState, useEffect } from 'react';
Here, we are creating the app’s UI that consists of two parts: the search input and the search result.
Because the input has a callback, when you type the text on the input, the text is passed as an argument to setSearchText
to update the value of search_text
using the useState
hook. Then, the search result shows up. For this demo, the result is 5,000 rows where each row consists of a rotating React logo and the same search query text.
Our light and immediate UI render is the search input with its text. When you type text on the search input, the text should appear immediately. However, displaying 5,000 React logos and the search text is an expensive UI render.
Let’s look at an example; try typing “I love React very much” quickly in our new React app. When you type “I”, the app renders the text “I” immediately on the search input. Then it renders the 5,000 rows. This takes a long time, which reveals our rendering problem. The React app cannot render the full text immediately. The expensive UI render makes the light UI render become slow as well.
You can try it yourself on the app at http://localhost:3000. You’ll be presented with a search input. I have set up a demo app as well.
What we want is for the expensive UI render not to drag the light UI render to the mud while it loads. They should be separated, which is where startTransition
comes in.
Using startTransition
Let’s see what happens when we import startTransition
. Your top line import should be like this:
import {useState, useEffect, startTransition} from 'react';
Then, wrap the expensive UI render in this function. Change setSearchResult(list)
into the code below:
startTransition(() => {
setSearchResult(list);
});
Now, you can test the app again. When you type something in the search input, the text is rendered immediately. After you stop (or a couple of seconds pass), the React app renders the search result.
What if you want to display something on the search results while waiting for the expensive UI render to finish? You may want to display a progress bar to give immediate feedback to users so they know the app is working on their request.
For this, we can use the isPending
variable that comes from the useTransition
hook.
First, change the import line on the top of the file into the code below:
import {useState, useEffect, useTransition} from 'react';
Extract isPending
and startTransition
from the useTransition
hook. Put the code below on the first line inside the App
function:
const [isPending, startTransition] = useTransition();
Next, change the content of <div className="SearchResult">
to the code below:
{isPending && <div><br /><span>Loading...</span></div>}
{!isPending && search_result}
Now when you type the text on the search input very fast, the loading indicator is displayed first.
Conclusion
With startTransition
, you can make the React app smooth and reactive by separating the immediate UI renders and the non-urgent UI renders. By putting all non-urgent UI renders inside the startTransition
method, your app will be much more satisfying to use.
We also covered using the isPending
variable to indicate the status of the transition in case you want to give feedback to users.
You can get the full code of the startTransition
demo app here. You can also experiment with the demo of the app to your heart's content. Hopefully this knowledge will be useful for you when you build your next React app. Make sure the apps will be smooth!
Full visibility into production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
Top comments (0)