Understanding Concurrent Rendering in React 18


📅 Published at: February 5, 2022

With React 18, concurrent rendering introduces a fundamental shift in how React handles UI updates. This new approach makes applications more responsive, reduces jank, and allows React to prioritize rendering tasks intelligently.

In this post, we’ll break down what concurrent rendering is, how it works, and why it’s a game-changer for modern React apps.


🔹 What Is Concurrent Rendering?

In previous versions of React (before 18), rendering was synchronous and blocking. This meant that once React started rendering, it would fully complete the update before responding to any new user interactions—even if that meant freezing the UI momentarily.

With Concurrent Rendering, React can start rendering, pause, and resume work as needed, making updates non-blocking and UI interactions much smoother.

How Does Concurrent Rendering Help?

✅ Prevents UI from freezing during expensive renders.
✅ Allows React to prioritize urgent updates over non-urgent ones.
✅ Improves perceived performance by keeping interactions responsive.


🚀 Key Features Enabling Concurrent Rendering

1️⃣ Automatic Batching

Before React 18, multiple state updates inside event handlers caused multiple re-renders. Now, React automatically batches these updates together, resulting in fewer renders and better performance.

Example: Before React 18 (Multiple Re-renders)

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  const handleClick = () => {
    setCount(count + 1); // Triggers a re-render
    setText("Updated!"); // Triggers another re-render
  };

  return (
    <div>
      <p>Count: {count}</p>
      <p>{text}</p>
      <button onClick={handleClick}>Click Me</button>
    </div>
  );
}

➡️ Each state update triggers a separate re-render.

After React 18 (Automatic Batching)

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  const handleClick = () => {
    setCount(count + 1);
    setText("Updated!");
  };

  return (
    <div>
      <p>Count: {count}</p>
      <p>{text}</p>
      <button onClick={handleClick}>Click Me</button>
    </div>
  );
}

React batches the updates into one re-render, making the app more efficient.


2️⃣ Prioritizing Updates with useTransition

With concurrent rendering, React lets us mark some state updates as “non-urgent” so that important interactions stay smooth.

🔹 New Hook: useTransition()

  • Keeps UI responsive by deferring non-urgent updates.
  • Urgent updates (like typing in an input) happen first.
  • Non-urgent updates (like filtering a large list) run in the background.

Example: Without useTransition (Laggy UI)

import { useState } from "react";

function SearchList({ items }) {
  const [query, setQuery] = useState("");
  const [filteredItems, setFilteredItems] = useState(items);

  const handleSearch = (e) => {
    setQuery(e.target.value);
    setFilteredItems(items.filter(item => item.includes(e.target.value))); 
    // This update blocks the UI while filtering
  };

  return (
    <div>
      <input type="text" value={query} onChange={handleSearch} placeholder="Search..." />
      <ul>
        {filteredItems.map((item, index) => <li key={index}>{item}</li>)}
      </ul>
    </div>
  );
}

➡️ If items is a large dataset, the UI lags while filtering.

Optimized with useTransition()

import { useState, useTransition } from "react";

function SearchList({ items }) {
  const [query, setQuery] = useState("");
  const [filteredItems, setFilteredItems] = useState(items);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (e) => {
    setQuery(e.target.value);
    startTransition(() => {
      setFilteredItems(items.filter(item => item.includes(e.target.value)));
    });
  };

  return (
    <div>
      <input type="text" value={query} onChange={handleSearch} placeholder="Search..." />
      {isPending && <p>Loading...</p>}
      <ul>
        {filteredItems.map((item, index) => <li key={index}>{item}</li>)}
      </ul>
    </div>
  );
}

UI stays responsive because the filtering runs in the background.


3️⃣ Suspense for Data Fetching (Improved!)

React Suspense lets us handle async operations more smoothly, improving both loading states and performance.

Before React 18 (Manual Loading States)

import { useEffect, useState } from "react";

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/user/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data));
  }, [userId]);

  if (!user) return <p>Loading...</p>;

  return <h1>Welcome, {user.name}!</h1>;
}

➡️ We manually handle loading states with useState().

With React 18’s Suspense

import { Suspense, lazy } from "react";

const UserProfile = lazy(() => import("./UserProfile"));

function App() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <UserProfile />
    </Suspense>
  );
}

✅ Suspense automatically handles loading, improving UX.


🛠️ How to Enable Concurrent Rendering

If you upgrade to React 18, you don’t need to do anything—Concurrent Rendering is enabled automatically!

Make sure to update your root render method:

Before (React 17)

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(<App />, document.getElementById("root"));

After (React 18)

import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

const root = createRoot(document.getElementById("root"));
root.render(<App />);

✅ This enables automatic concurrent rendering.


📌 Why You Should Care About Concurrent Rendering

🚀 React 18 makes apps faster and more responsive!
✔️ Less lag – Updates run in the background.
✔️ Better user experience – UI stays interactive even under load.
✔️ Improved rendering performance – React prioritizes updates intelligently.


🔚 Conclusion

Concurrent Rendering in React 18 changes how React handles rendering, improving responsiveness, interactivity, and performance. With automatic batching, useTransition(), and Suspense, React apps can now prioritize updates efficiently and feel much smoother.

💡 Have you upgraded to React 18? Let me know your thoughts in the comments! 🚀


Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

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