Concurrent Rendering in React ⛽🛣️

In this post, we will discuss the concept of concurrent rendering in React 18, which allows us to improve user experience in new and exciting ways.

So, buckle up, and let’s dive into the world of concurrency in React!

What is Concurrency ?

Concurrency means executing multiple tasks at the same time but not necessarily simultaneously. In a concurrent application, two tasks can start, run and complete in overlapping time periods, without waiting for one to finish before starting another. i.e Task-2 can start even before Task-1 gets completed.
Concurrency

Concurrency in React, Explained Simply

In the context of React.js, concurrency refers to having more than one task in progress at once without blocking the main thread, and concurrent tasks can overlap depending on which is more urgent. This is a stark contrast to the traditional, synchronous rendering model where React would block the main thread until it finished rendering the component tree.

For example, while you’re writing, you’re also preparing vegetables to eat. When the time comes to add ingredients to the vegetables, that will be more urgent, so you’ll pause writing and attend to that and come back to continue writing when you’re done.
At different points throughout the writing and cooking processes, your focus will be on what is more urgent.

React could only handle one task at a time in the past, and a task could not be interrupted once it had started. This approach is referred to as Blocking Rendering. To fix this problem, Concurrent Mode was introduced—which makes rendering interruptible.

What is Concurrent Rendering in React ?

In the realm of React, concurrent rendering is a game-changer. It’s a new way of rendering that helps your app stay responsive under heavy load and gives you more control over how updates get scheduled.
Concurrent rendering is all about creating a fluid user experience. It allows React to interrupt an ongoing rendering process to handle more urgent tasks. This way, even if your app is in middle of a large rendering task, it can still respond immediately to user interactions.

Say Bye to Concurrent Mode

Concurrent Mode was introduced as an experimental feature. In favor of a more logical adoption that allows you to opt in to concurrent rendering at your conveniences. Concurrent Mode is now being replaced in React 18 with concurrent features.

Here’s an example to illustrate the difference:

// React 16
ReactDOM.render(<App />, document.getElementById('root')); // React 18
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
Enter fullscreen mode
Exit fullscreen mode

In React 16, we use ReactDOM.render to render our app.
In React 18, we use ReactDOM.createRoot to create a root node and then call render on that root node. This enables concurrent rendering for the entire app.

Concurrent Features in React 18

Let’s look at a few more concurrency features that React 18 introduces.

Automatic batching

Batching is a React feature which performs multiple state updates in a single rendering operation. In previous versions of React core, by default , only the state updates defined inside React event handlers were batched.

React 18 comes with automatic batching, which batches all updates by default, regardless of their origin. This includes updates inside promises, timeouts, and native event handlers. Automatic batching significantly reduces rendering iterations, which enhances performance and responsiveness.

Here’s the example:

const [count, setCount] = useState(0); // In React 16, these would cause two re-renders setCount(count + 1); setCount(count + 1); // In React 18, these are batched together and cause a single re-render
Enter fullscreen mode
Exit fullscreen mode

SSR with Suspense

Server-side rendering is a technique that allows us to generate HTML from React components on the server and then send a fully rendered HTML page to client. After the HTML is rendered in the browser, the React and JavaScript code for the entire app starts loading. When it is done, It adds interactivity to static HTML generated on the server—a process known as hydration.

In the previous versions of React, hydration could only begin after the entire data had been fetched from the server and rendered to HTML. Additionally, your users couldn’t interact with the page until hydration was complete for the whole page.

To solve this problem, the Suspense component was released in 2018. The only supported use case was lazy-loading code on the client, not during server rendering. But React 18 added support for Suspense on the server. React 18 offers two major features for SSR unlocked by Suspense:

1. Streaming Server Rendering

React 18 introduces new server rendering APIs that allow you to stream HTML from the server to the client. This means Streaming HTML is just sending lightweight static HTML. It’s replaced by heavier components that depend, without waiting for the entire response.

<Layout> <Article /> <Suspense fallback={<Spinner />}> <Comments /> </Suspense>
</Layout>

Enter fullscreen mode
Exit fullscreen mode

2. Selective hydration

Selective hydration mechanisms decide which components need to be loaded first. React detects events started by the user’s interaction with the component and hydrate certain components first.

If you have multiple Suspense, React will attempt to hydrate all of them but it will start with Suspense found earlier in the tree first.

Transitions

A transition is a new concept in React to distinguish between urgent and non-urgent updates.

  • Urgent updates reflect direct interaction, like typing, clicking, pressing, and so on.
  • Transition updates transition the UI from one view to another.

Transitions API gives us an imperative way of marking some update as low priority.

StartTransition API

The startTransition API can be used to mark any state updates as non-urgent. The startTransition API receives a function as argument such that any state updates that occur within this function are marked as a low priority update.

startTransition: a method to start transitions when the hook cannot be used.

In the code block below, there are two state updates- one that is urgent and other is marked as non-urgent as it’s wrapped inside the startTransition callback:

import {startTransition} from 'react'; // Urgent: Show what was typed
setInputValue(input); // Mark any state updates inside as transitions (non-urgent)
startTransition(() => { // Transition: Show the results setResults(input);
});
Enter fullscreen mode
Exit fullscreen mode

Non-urgent updates will be interrupted if more urgent updates like clicks or key presses come in. If a transition gets interrupted by the user (for example, by typing multiple characters in a row) or start transition is called again before the previous call is completed, React by default cancels older transitions and render only the latest update.

useTransition Hook

React can also track and update pending state transitions using the useTransition hook.

useTransition: a hook to start transitions, including a value to track the pending state.

Invoking const[isPending, startTransition] = useTransition() returns an array of two items:

  • isPending: indicates that the transition is pending.
  • startTransition(callback): allows you to mark any UI updates inside callback as transitions or which dispatches a new concurrent render.

As already mentioned, you can use useTransition() hook to let know React which UI updates are urgent (like updating the input field value), and which are non-urgent transitions (like updating the names list to highlight the query).

First, let’s invoke the [isPending, startTransition] = useTransition() hook to get access to startTransition() function. Secondly, let’s create a state variable to hold the query state value specifically for the transition.

import { useState, useCallback, useTransition } from 'react'; function MyCounter() { const [isPending, startTransition] = useTransition(); const [count, setCount] = useState(0); const increment = useCallback(() => { startTransition(() => { // Run this update concurrently setCount(count => count + 1); }); }, []); return ( <> <button onClick={increment}>Count {count}</button>
 <span>{isPending ? "Pending" : "Not Pending"}</span>
 // Component which benefits from concurrency <ManySlowComponents count={count} />
 </>
 )
}
Enter fullscreen mode
Exit fullscreen mode

An important caveat of useTransition is that it cannot be used for controlled inputs.

const [text, setText] = useState('');
// ...
function handleChange(e) { // ❌ Can't use Transitions for controlled input state startTransition(() => { setText(e.target.value); });
}
// ...
return <input value={text} onChange={handleChange} />;
Enter fullscreen mode
Exit fullscreen mode

This is because Transitions are non-blocking, but updating an input in response to the change event should happen synchronously. If you want to run a Transition in response to typing, you have two options:

  • You can declare two separate state variables: one for the input state (which always updates synchronously), and one that you will update in a Transition. This lets you control the input using the synchronous state, and pass the Transition state variable (which will “lag behind” the input) to the rest of your rendering logic.
  • Alternatively, you can have one state variable, and add useDeferredValue which will “lag behind” the real value. It will trigger non-blocking re-renders to “catch up” with the new value automatically.

For those cases, it is best to use useDeferredValue.

useDeferredValue Hook

The useDeferredValue hook is a convenient hook for cases where you do not have the opportunity to wrap the state update in startTransition but still wish to run the update concurrently.

An example of where this occurs is child components receiving a new value from the parent.

Conceptually, useDeferredValue similar to debouncing, but has a few advantages compared to it. There is no fixed time delay, so React will attempt the deferred render right after the first render is reflected on the screen. The deferred render is interruptible and doesn’t block user input.
useDeferredValue can be implemented as such:

function useDeferredValue(value) { const [isPending, startTransition] = useTransition(); const [state, setState] = useState(value); useEffect(() => { // Dispatch concurrent render // when input changes startTransition(() => { setState(value); }); }, [value]) return state;
} 
Enter fullscreen mode
Exit fullscreen mode

The Future of React with Concurrent Mode

Concurrent mode is not just a new feature, it’s a whole new way of thinking about React. It allows React to work on multiple tasks at once, without blocking the main thread. It introduces powerful new features like automatic batching, streaming server rendering, and React Suspense. And it changes how we think about rendering and state updates in React.

So, as we look to the future of React with concurrent mode. Use these new powers to create more responsive, user-friendly apps.


Discover more from Coursity

Subscribe to get the latest posts sent to your email.

Leave a Comment

Your email address will not be published. Required fields are marked *

Discover more from Coursity

Subscribe now to keep reading and get access to the full archive.

Continue reading

Scroll to Top
Free & easy ad network.