DEV Community

Cover image for Concurrent Rendering in React ⛽🛣️
Majedur Rahman
Majedur Rahman

Posted on • Edited on

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

React 18 introduces great key feature is automatic batching. which enhances rendering effectiveness, performance and responsiveness.

Batching is when React groups multiple state updates into a single re-render for better performance.

In React 17 and prior, were batched update occur only the state updates defined inside React event handlers. But updates inside of promises, setTimeout, native events handlers, or any other event were not batched in React by default.

Here's the example, React 17 and prior versions:

const [increase, setIncrease] = useState(0);
const [decrease, setDecrease] = useState(0);

const handleOnClick = () => {
     setIncrease(increase + 1)
     setDecrease(decrease - 1)
   };

console.log('Rendering');

return (
    <div className="wrapper">
      <h1>Counter details</h1>
      <p>
        Increase: <strong>{increase}</strong>
      </p>
      <p>
        Decrease: <strong>{decrease}</strong>
      </p>
      <button onClick={handleOnClick}>Click Me</button>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

In the above example, the handleOnClick() function will be called when a user clicks on the button. It will execute two-state updates on each click.

If you observe the browser console, you will see that the “Rendering” message is logged only once for both state updates.

Now you have seen that React batched updates both states and re-rendered the component only once inside event handler.

But, what if we execute state updates in a context that is not associated with the browser?

For example, consider a setTimeout() call that asynchronously loads data:

  const [increase, setIncrease] = useState(0);
  const [decrease, setDecrease] = useState(0);

  const handleOnClick = () => {
  // updates state inside of setTimeout
    setTimeout(() => {
      setIncrease(increase + 1)
      setDecrease(decrease - 1)
    }, 0)
  }

  console.log('Rendering');

  return (
    <div className="wrapper">
      <h1>Counter details</h1>
      <p>
        Increase: <strong>{increase}</strong>
      </p>
      <p>
        Decrease: <strong>{decrease}</strong>
      </p>
      <button onClick={handleOnClick}>Click Me</button>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

In the above example, the state update occurs in the callback of setTimeout() function.

If you observe the browser console after executing this example, you will see 2 messages. This indicates that two separate re-renders occur for each state update.

This behaviour can cause severe performance issues related to application speed. That’s why React introduced Automatic Batching.

In React 18 we are introduced to improved version of batching called Automatic Batching. It will enabled batching of all the state updates regardless of where they are called. This includes updates inside promises, timeouts, and native event handlers.

Let’s consider the previous example to understand how automatic batching works:

  const [increase, setIncrease] = useState(0);
  const [decrease, setDecrease] = useState(0);

  const handleOnClick = () => {
  // updates state inside of setTimeout
    setTimeout(() => {
      setIncrease(increase + 1)
      setDecrease(decrease - 1)
    }, 0)
  }

  console.log('Rendering');

  return (
    <div className="wrapper">
      <h1>Counter details</h1>
      <p>
        Increase: <strong>{increase}</strong>
      </p>
      <p>
        Decrease: <strong>{decrease}</strong>
      </p>
      <button onClick={handleOnClick}>Click Me</button>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

The above example shows an optimal re-rendering process where handleOnClick() function will be called when a user clicks on the button.It will execute two-state updates on each click and give only one re-render as React batches all state updates regardless of the invocation location.

However, Automatic batching significantly reduces rendering iterations, which enhances performance and responsiveness.

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.

Why Suspense ?

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 component was released in 2018. The only supported use case was lazy-loading code on the client, not during server rendering.

Let’s start with how the <Suspense> component works with the lazy loading components.

When using the Suspense component, you must provide a fallback option. It accepts any React components or even a string to render while the lazy component is loading.

Take a look at the example below, which uses the Suspense wrapper to display a typical loader until the <LazyLoadComponent> is ready.

import { lazy, Suspense } from 'react';
const LazyLoadComponent = lazy(
      () => import('./LazyLoadComponent')
);

function MyComponent() {
  return (
    <Suspense fallback={<Loader />}>
      <div>
        <LazyLoadComponent />
      </div>
    </Suspense>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now, let’s take a look at the enhancements Suspense received in React 18.

Suspense is no longer limited to lazy components, 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 React hook that allows you to defer updating a part of the UI. It 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.

The useDeferredValue hook is a useful addition to React 18, aimed to improve component rendering performance by preventing unnecessary re-renders.

The useDeferredValue hook takes two arguments:

  1. value: The value you want to defer. It can have any type.
  2. options: An object that can have the 2 properties timeoutMs and scheduler
  • timeoutMs: The time in milliseconds to wait before updating the value.
  • scheduler: A function that will be used to schedule the update.

The useDeferredValue hook returns a value that is the deferred version of the value you provided. The deferred value will be updated after the specified amount of time, or immediately if the value changes.

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.

Here is an example of how to use the useDeferredValue hook:

import React, { useState, useDeferredValue } from 'react';

const [count, setCount] = useState(0);
  const deferredCount = useDeferredValue(count, {
    timeoutMs: 1000,
  });
  const handleClick = () => {
    setCount(count + 1);
  };
  return (
    <div>
      <h1>Counter</h1>
      <p>Current count: {count}</p>
      <button onClick={handleClick}>Click me</button>
      <p>Deferred count: {deferredCount}</p>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

In this example, the count state variable is updated immediately when the user clicks the button. However, the deferredCount value is not updated immediately. Instead, it is updated after 1000 milliseconds. This means that the deferredCount value will not reflect the latest value of the count state variable until after 1000 milliseconds have passed.

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.

Top comments (0)