# React 19 Suspense and Concurrent Rendering: Streaming SSR and Interview Questions 2026 > Master React 19 Suspense, concurrent rendering, and streaming SSR. Learn how useTransition, useDeferredValue, and the use() API work together for responsive UIs, with real interview questions and production patterns. - Published: 2026-08-21 - Updated: 2026-08-21 - Author: Anthony Fillion-Maillet - Tags: react, suspense, concurrent-rendering, streaming-ssr, interview - Reading time: 12 min --- React 19 Suspense transforms how applications handle asynchronous operations by providing a declarative model for loading states, error handling, and data fetching. Combined with concurrent rendering, Suspense enables streaming SSR that delivers faster time-to-content while keeping the UI responsive during heavy computations. > **Key Takeaway** > > Suspense boundaries define where loading fallbacks appear. Concurrent rendering lets React interrupt low-priority work to keep high-priority interactions (typing, clicking) responsive. Together, they enable progressive hydration and streaming HTML from the server. ## How Concurrent Rendering Differs from Synchronous React Before React 18, rendering was synchronous: once React started rendering a component tree, it had to finish before handling any other work. Concurrent rendering breaks this model by allowing React to pause, interrupt, and resume rendering based on priority. The [React documentation](https://react.dev/blog/2022/03/29/react-v18#what-is-concurrent-react) describes concurrent rendering as a behind-the-scenes mechanism that enables features like Suspense, transitions, and streaming SSR. The key insight: React can prepare multiple versions of the UI simultaneously. ```tsx // TransitionExample.tsx import { useState, useTransition } from 'react'; function SearchResults({ query }: { query: string }) { // Expensive computation or data fetching const results = searchDatabase(query); return ; } export function SearchPage() { const [query, setQuery] = useState(''); const [isPending, startTransition] = useTransition(); function handleChange(e: React.ChangeEvent) { const value = e.target.value; // High priority: update input immediately setQuery(value); // Low priority: update results in background startTransition(() => { setDeferredQuery(value); }); } return (
{isPending && }
); } ``` The `useTransition` hook marks state updates as non-urgent. React continues showing the previous UI while computing the new one in the background, then swaps when ready. ## Suspense Boundaries and the Loading Hierarchy Suspense boundaries define fallback UI for components that suspend. When a component inside a Suspense boundary throws a promise (signals that data is loading), React shows the fallback until the promise resolves. ```tsx // ProductPage.tsx import { Suspense } from 'react'; function ProductDetails({ id }: { id: string }) { // This component suspends while fetching const product = use(fetchProduct(id)); return
{product.name}
; } function ProductReviews({ id }: { id: string }) { const reviews = use(fetchReviews(id)); return ; } export function ProductPage({ id }: { id: string }) { return (
{/* Outer boundary: catches both if needed */} }> {/* Inner boundary: reviews can load independently */} }>
); } ``` Nested Suspense boundaries enable granular loading states. The product details can render as soon as they load, while reviews show their own skeleton. ## The use() API for Data Fetching React 19 introduces the `use()` API, documented in the [React RFC](https://github.com/reactjs/rfcs/blob/main/text/0229-use.md), as the official way to read promises and context in render. Unlike hooks, `use()` can be called conditionally. ```tsx // UserProfile.tsx import { use, Suspense } from 'react'; // Promise created outside component (render-as-you-fetch) const userPromise = fetchUser(userId); function UserProfile() { // use() suspends until promise resolves const user = use(userPromise); return (

{user.name}

{user.email}

); } export function UserPage() { return ( }> ); } ``` The render-as-you-fetch pattern starts data fetching before rendering begins, typically in route loaders or server components. This preserves parallelism because multiple fetches can run simultaneously rather than waterfalling. ## Streaming SSR with React Server Components Streaming SSR sends HTML to the client in chunks as data becomes available. Combined with [React Server Components](/blog/react-next/react-server-components-patterns-pitfalls), this approach delivers faster first contentful paint while progressively hydrating interactive parts. ```tsx // app/products/[id]/page.tsx (Next.js App Router) import { Suspense } from 'react'; // Server Component: runs on server only async function ProductInfo({ id }: { id: string }) { const product = await db.products.findUnique({ where: { id } }); return (

{product.name}

{product.description}

); } // Server Component with slow data async function RelatedProducts({ categoryId }: { categoryId: string }) { const products = await db.products.findMany({ where: { categoryId }, take: 6 }); return ; } export default async function ProductPage({ params }: { params: { id: string } }) { const product = await db.products.findUnique({ where: { id: params.id } }); return (
{/* Streams immediately */} {/* Streams when ready, shows skeleton first */} }>
); } ``` The server streams the ProductInfo HTML first. When RelatedProducts data resolves, React sends the additional HTML with instructions to replace the skeleton. ## useDeferredValue for Expensive Computations While `useTransition` wraps state updates, `useDeferredValue` defers the rendering of a specific value. This works well for filtering large lists or expensive derived computations. ```tsx // FilterableList.tsx import { useState, useDeferredValue, useMemo } from 'react'; function ExpensiveList({ filter }: { filter: string }) { // Expensive filtering operation const filteredItems = useMemo(() => { return items.filter(item => item.name.toLowerCase().includes(filter.toLowerCase()) ); }, [filter]); return (
    {filteredItems.map(item => (
  • {item.name}
  • ))}
); } export function SearchableList() { const [filter, setFilter] = useState(''); // Deferred version lags behind during typing const deferredFilter = useDeferredValue(filter); const isStale = filter !== deferredFilter; return (
setFilter(e.target.value)} placeholder="Search..." />
); } ``` The input stays responsive because React prioritizes the state update. The list re-renders with the deferred value when React has idle time. ## Interview Questions: Concurrent Rendering Technical interviews for [React positions](/technologies/react-next/interview-questions/react-performance-optimization) frequently probe understanding of concurrent features. These questions separate candidates who have shipped production Suspense code from those who only know the theory. **Q: What happens when a component suspends?** When a component throws a promise during render, React catches it at the nearest Suspense boundary. React renders the fallback UI and subscribes to the promise. When it resolves, React re-renders the suspended component. If the promise rejects, the error propagates to the nearest error boundary. **Q: Why does render-as-you-fetch matter for Suspense?** Fetching during render creates request waterfalls: Parent fetches, then renders Child, which fetches, then renders Grandchild, which fetches. Each step waits for the previous. Render-as-you-fetch starts all fetches before rendering, so they run in parallel. Route loaders and server components naturally support this pattern. **Q: When would useTransition hurt performance?** Transitions keep the old UI visible while computing the new one. This means React renders twice: once for the stale UI and once for the fresh UI. For fast operations, this overhead exceeds the benefit. Use transitions only for updates that take noticeable time, typically 100ms or more. ## Error Boundaries with Suspense Suspense handles loading states, but [error boundaries](/technologies/react-next/interview-questions/react-error-boundaries) handle failures. Production applications need both: ```tsx // DataBoundary.tsx import { Component, Suspense, ReactNode } from 'react'; interface Props { children: ReactNode; fallback: ReactNode; errorFallback: ReactNode; } interface State { hasError: boolean; error: Error | null; } class ErrorBoundary extends Component { state: State = { hasError: false, error: null }; static getDerivedStateFromError(error: Error): State { return { hasError: true, error }; } render() { if (this.state.hasError) { return this.props.errorFallback; } return this.props.children; } } export function DataBoundary({ children, fallback, errorFallback }: Props) { return ( {children} ); } ``` The error boundary wraps the Suspense boundary so rejected promises trigger the error UI rather than crashing the app. ## Suspense Batching in React 19.2 React 19.2 introduced Suspense batching for server rendering, addressing a common pain point: multiple sibling components suspending caused multiple round trips. With batching, React waits briefly to collect multiple suspensions, then resolves them together. ```tsx // Before 19.2: Each Suspense boundary streams independently // After 19.2: React batches nearby suspensions export function Dashboard() { return (
{/* These now batch together */} }> }> }>
); } ``` Batching reduces the visual flicker of cards appearing one by one and improves perceived performance by presenting related data together. ## Performance Patterns for Production These patterns address common production issues with Suspense and concurrent rendering. **Avoid Suspense Waterfalls** ```tsx // Problem: Sequential fetching function Parent() { const data = use(fetchParent()); return ; // Child fetches after Parent } // Solution: Parallel fetching with Promise.all const [parentPromise, childPromise] = [ fetchParent(), fetchChild() ]; function OptimizedParent() { const parent = use(parentPromise); return ; } ``` **Preload on Hover** ```tsx // LinkWithPrefetch.tsx function LinkWithPrefetch({ href, children }: Props) { const router = useRouter(); function handleMouseEnter() { // Start fetching before click router.prefetch(href); } return ( {children} ); } ``` ## Key Patterns for React 19 Concurrent Features - Place Suspense boundaries at UI seams where independent loading makes sense, not around every async component - Trigger data fetching in route loaders or server components, not inside render functions, to enable parallel requests - Use `useTransition` for user-initiated navigations and form submissions that take over 100ms - Apply `useDeferredValue` to expensive derived computations like filtering 10,000+ items - Combine error boundaries with Suspense boundaries so rejected promises show error UI instead of crashing - Prefer streaming SSR for content-heavy pages where time-to-first-byte matters more than full hydration - Test concurrent features with React DevTools Profiler to identify unnecessary re-renders during transitions --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/react-next/react-19-suspense-concurrent-rendering-streaming-ssr