React 19 introduces powerful concurrent features that make applications more responsive and performant. Concurrent rendering allows React to work on multiple tasks simultaneously, improving user experience especially on slower devices.
Concurrent Rendering
Concurrent rendering enables React to interrupt and pause work to handle more urgent updates, then continue where it left off. This prevents blocking the main thread and keeps the UI responsive.
Automatic Batching
React 19 automatically batches all state updates, even in async functions and promises, reducing unnecessary re-renders.
// React 18: Multiple renders
function handleClick() {
setCount(c => c + 1); // Render 1
setFlag(f => !f); // Render 2
setValue(v => v + 1); // Render 3
}
// React 19: Single render (automatic batching)
async function handleClick() {
await fetchData();
setCount(c => c + 1); // Batched
setFlag(f => !f); // Batched
setValue(v => v + 1); // Batched
// Single render!
}
Server Components
React Server Components run on the server, allowing you to fetch data directly in components without exposing API routes. They reduce bundle size and improve performance.
// Server Component (app/users/page.tsx)
async function UsersPage() {
// Direct database access - no API route needed
const users = await db.users.findMany();
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}
// Client Component (for interactivity)
'use client';
function UserCard({ user }) {
const [liked, setLiked] = useState(false);
return (
<div>
<h3>{user.name}</h3>
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
</div>
);
}
Suspense Improvements
React 19 enhances Suspense with better error boundaries, parallel data fetching, and improved streaming.
Parallel Data Fetching
async function ProfilePage() {
// These fetch in parallel
const [user, posts, friends] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchFriends(),
]);
return (
<div>
<UserProfile user={user} />
<PostsList posts={posts} />
<FriendsList friends={friends} />
</div>
);
}
// With Suspense boundaries
function Page() {
return (
<Suspense fallback={<ProfileSkeleton />}>
<ProfilePage />
</Suspense>
);
}
useTransition and useDeferredValue
These hooks help prioritize updates and keep the UI responsive during heavy computations.
useTransition for Non-Urgent Updates
function SearchResults({ query }) {
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState([]);
useEffect(() => {
startTransition(() => {
// This update can be interrupted
const data = searchData(query);
setResults(data);
});
}, [query]);
return (
<div>
{isPending && <LoadingSpinner />}
{results.map(result => <Item key={result.id} {...result} />)}
</div>
);
}
useDeferredValue for Deferred Updates
function ProductList({ searchTerm }) {
const deferredSearchTerm = useDeferredValue(searchTerm);
const products = useMemo(
() => filterProducts(deferredSearchTerm),
[deferredSearchTerm]
);
return (
<div>
{searchTerm !== deferredSearchTerm && <Loading />}
{products.map(product => <Product key={product.id} {...product} />)}
</div>
);
}
Actions and Form Handling
React 19 introduces Server Actions and improved form handling with automatic optimistic updates.
// Server Action
'use server';
async function createPost(formData: FormData) {
const title = formData.get('title');
const content = formData.get('content');
await db.posts.create({
data: { title, content },
});
revalidatePath('/posts');
}
// Client Component
function CreatePostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" />
<textarea name="content" placeholder="Content" />
<button type="submit">Create Post</button>
</form>
);
}
// With useActionState for optimistic updates
function OptimisticForm() {
const [state, formAction, isPending] = useActionState(createPost, null);
return (
<form action={formAction}>
<input name="title" />
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create'}
</button>
{state?.error && <p>{state.error}</p>}
</form>
);
}
use Hook for Async Operations
The new use hook simplifies working with Promises and Context in components.
// Using with Promises
function UserProfile({ userPromise }) {
const user = use(userPromise);
return <div>{user.name}</div>;
}
// Using with Context
function ThemeButton() {
const theme = use(ThemeContext);
return <button className={theme}>Themed Button</button>;
}
// With Suspense
function Page() {
const userPromise = fetchUser();
return (
<Suspense fallback={<Loading />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
Document Metadata
React 19 simplifies setting document metadata directly from components.
function BlogPost({ post }) {
return (
<>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<meta property="og:image" content={post.image} />
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
</>
);
}
Performance Best Practices
- Use Server Components for data fetching to reduce client bundle
- Leverage useTransition for non-urgent state updates
- Implement Suspense boundaries strategically
- Use useMemo and useCallback judiciously (React 19 optimizes better)
- Prefer Server Actions for form submissions
Real-World Example
We migrated a large dashboard to React 19:
- Converted data-fetching components to Server Components
- Used useTransition for heavy filtering operations
- Implemented Suspense for progressive loading
- Result: 50% reduction in JavaScript bundle, 40% faster Time to Interactive
Conclusion
React 19's concurrent features revolutionize how we build React applications. Server Components reduce bundle sizes, automatic batching improves performance, and Suspense enhances user experience. Start migrating critical data-fetching to Server Components, use useTransition for heavy operations, and leverage Server Actions for forms. These features work together to create faster, more responsive applications.