React has come a long way from simple component-based UI building. With the introduction of React 18 , features like Suspense and Concurrent Rendering have changed the way we think about performance, data fetching, and user experience. If you have ever struggled with slow-loading components or flickering UIs, these features are built for you.
In this article, we will explore what React Suspense and Concurrent Rendering are, how they work, and when to use them effectively.
What is React Suspense?
React Suspense is a mechanism that lets components wait for something before rendering. You can show a fallback UI while React waits for the required data or code to be ready. It improves the loading experience and makes the UI feel smoother.
Before Suspense, developers had to manually manage loading states like this:
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
if (!user) {
return <p>Loading...</p>;
}
return <h2>{user.name}</h2>;
}
This approach works, but it adds a lot of boilerplate . Suspense simplifies this process.
Using React Suspense with Lazy Loading
One of the most common use cases for Suspense is code-splitting with React.lazy
.
import React, { Suspense, lazy } from "react";
const UserProfile = lazy(() => import("./UserProfile"));
export default function App() {
return (
<div>
<h1>React Suspense Example</h1>
<Suspense fallback={<p>Loading profile...</p>}>
<UserProfile />
</Suspense>
</div>
);
}
Here’s what happens:
React does not load UserProfile
until it is needed.
While it is loading, React shows the fallback UI.
Once the component is ready, React swaps the fallback with the actual content.
This improves performance and reduces the initial bundle size.
React Suspense for Data Fetching
Suspense also works beautifully with data fetching when combined with libraries like React Query or Relay. With React’s upcoming improvements, Suspense will natively support async data fetching .
Example using a simple Suspense-friendly resource:
const fetchUser = (id) => {
return fetch(`/api/users/${id}`).then(res => res.json());
};
const resource = {
user: fetchUser(1)
};
function User() {
const user = React.use(resource.user); // Future API for Suspense
return <h2>{user.name}</h2>;
}
export default function App() {
return (
<Suspense fallback={<p>Loading user...</p>}>
<User />
</Suspense>
);
}
Although the above pattern is experimental, it shows the direction React is moving toward: data fetching will become more declarative and less boilerplate-heavy.
What is Concurrent Rendering in React?
Concurrent Rendering is another React 18 feature designed to improve performance and responsiveness. Traditionally, React rendered components synchronously. If a large update took a long time, it would block the UI, making the app feel sluggish.
With concurrent rendering, React can pause , interrupt , and resume rendering without freezing the UI. It works behind the scenes, but you can control it using new APIs like startTransition
.
Using startTransition
for Smooth UI Updates
Let’s take a simple example where you have a search input. Without concurrent rendering, React tries to update the UI for every keystroke, which can cause lag if the data list is huge.
import React, { useState, useTransition } from "react";
export default function Search() {
const [query, setQuery] = useState("");
const [list, setList] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
startTransition(() => {
const filtered = bigData.filter(item =>
item.toLowerCase().includes(value.toLowerCase())
);
setList(filtered);
});
};
return (
<div>
<input type="text" value={query} onChange={handleChange} placeholder="Search..." />
{isPending && <p>Updating list...</p>}
<ul>
{list.map((item, idx) => <li key={idx}>{item}</li>)}
</ul>
</div>
);
}
Here is what’s happening:
startTransition
Tells React that this update is non-urgent.
React prioritizes keeping the UI responsive and delays heavy rendering work.
isPending
Gives you a way to show feedback while React works in the background.
Suspense + Concurrent Rendering = Better UX
React Suspense and concurrent rendering are even more powerful when combined. For example, when switching between tabs that fetch data, React can:
Suspend the component until the data is ready
Stream content progressively
Keep the UI responsive while heavy components load
This results in smoother navigation and less blocking, especially for large apps.
Best Practices for Using Suspense and Concurrent Rendering
Use Suspense for lazy loading and data fetching
Always provide a clear and meaningful fallback UI
Use startTransition
for non-urgent updates
Keep Suspense boundaries small and focused
Combine Suspense with streaming SSR in Next.js 14 for better performance
Final Thoughts
React Suspense and Concurrent Rendering represent the future of React performance optimization . Suspense helps you manage loading states elegantly, while concurrent rendering keeps the UI responsive even under heavy workloads. Together, they enable faster, smoother, and more delightful user experiences.
If you are building modern React apps, learning these concepts is no longer optional. Start by experimenting with Suspense for lazy loading, then explore concurrent rendering APIs like startTransition
to make your apps feel snappier.