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.

React 19 Suspense and Concurrent Rendering visualization

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 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.

TransitionExample.tsxtsx
import { useState, useTransition } from 'react';

function SearchResults({ query }: { query: string }) {
  // Expensive computation or data fetching
  const results = searchDatabase(query);
  return <ResultsList items={results} />;
}

export function SearchPage() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    const value = e.target.value;
    // High priority: update input immediately
    setQuery(value);
    // Low priority: update results in background
    startTransition(() => {
      setDeferredQuery(value);
    });
  }

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <SearchResults query={deferredQuery} />
    </div>
  );
}

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.

ProductPage.tsxtsx
import { Suspense } from 'react';

function ProductDetails({ id }: { id: string }) {
  // This component suspends while fetching
  const product = use(fetchProduct(id));
  return <div>{product.name}</div>;
}

function ProductReviews({ id }: { id: string }) {
  const reviews = use(fetchReviews(id));
  return <ReviewsList reviews={reviews} />;
}

export function ProductPage({ id }: { id: string }) {
  return (
    <div>
      {/* Outer boundary: catches both if needed */}
      <Suspense fallback={<PageSkeleton />}>
        {/* Inner boundary: reviews can load independently */}
        <ProductDetails id={id} />
        <Suspense fallback={<ReviewsSkeleton />}>
          <ProductReviews id={id} />
        </Suspense>
      </Suspense>
    </div>
  );
}

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, as the official way to read promises and context in render. Unlike hooks, use() can be called conditionally.

UserProfile.tsxtsx
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 (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

export function UserPage() {
  return (
    <Suspense fallback={<ProfileSkeleton />}>
      <UserProfile />
    </Suspense>
  );
}

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.

Ready to ace your React / Next.js interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Streaming SSR with React Server Components

Streaming SSR sends HTML to the client in chunks as data becomes available. Combined with React Server Components, this approach delivers faster first contentful paint while progressively hydrating interactive parts.

app/products/[id]/page.tsx (Next.js App Router)tsx
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 (
    <section>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </section>
  );
}

// Server Component with slow data
async function RelatedProducts({ categoryId }: { categoryId: string }) {
  const products = await db.products.findMany({
    where: { categoryId },
    take: 6
  });
  return <ProductGrid products={products} />;
}

export default async function ProductPage({ 
  params 
}: { 
  params: { id: string } 
}) {
  const product = await db.products.findUnique({ 
    where: { id: params.id } 
  });

  return (
    <main>
      {/* Streams immediately */}
      <ProductInfo id={params.id} />
      
      {/* Streams when ready, shows skeleton first */}
      <Suspense fallback={<GridSkeleton />}>
        <RelatedProducts categoryId={product.categoryId} />
      </Suspense>
    </main>
  );
}

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.

FilterableList.tsxtsx
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 (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

export function SearchableList() {
  const [filter, setFilter] = useState('');
  // Deferred version lags behind during typing
  const deferredFilter = useDeferredValue(filter);
  const isStale = filter !== deferredFilter;

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Search..."
      />
      <div style={{ opacity: isStale ? 0.7 : 1 }}>
        <ExpensiveList filter={deferredFilter} />
      </div>
    </div>
  );
}

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 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 handle failures. Production applications need both:

DataBoundary.tsxtsx
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<Props, State> {
  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 (
    <ErrorBoundary errorFallback={errorFallback}>
      <Suspense fallback={fallback}>
        {children}
      </Suspense>
    </ErrorBoundary>
  );
}

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.

Before 19.2: Each Suspense boundary streams independentlytsx
// After 19.2: React batches nearby suspensions

export function Dashboard() {
  return (
    <div className="grid grid-cols-3 gap-4">
      {/* These now batch together */}
      <Suspense fallback={<CardSkeleton />}>
        <RevenueCard />
      </Suspense>
      <Suspense fallback={<CardSkeleton />}>
        <UsersCard />
      </Suspense>
      <Suspense fallback={<CardSkeleton />}>
        <OrdersCard />
      </Suspense>
    </div>
  );
}

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 parentId={data.id} />; // Child fetches after Parent
}

// Solution: Parallel fetching with Promise.all
const [parentPromise, childPromise] = [
  fetchParent(),
  fetchChild()
];

function OptimizedParent() {
  const parent = use(parentPromise);
  return <OptimizedChild promise={childPromise} />;
}

Preload on Hover

LinkWithPrefetch.tsxtsx
function LinkWithPrefetch({ href, children }: Props) {
  const router = useRouter();
  
  function handleMouseEnter() {
    // Start fetching before click
    router.prefetch(href);
  }

  return (
    <Link href={href} onMouseEnter={handleMouseEnter}>
      {children}
    </Link>
  );
}

Start practicing!

Test your knowledge with our interview simulators and technical tests.

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
Daily challenge

Can you spot the bug in React / Next.js?

One real snippet, one hidden bug, one attempt a day. No account needed to try.

Anthony Fillion-Maillet

Written by

Anthony Fillion-Maillet

Founder of SharpSkill

Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.

Updated on August 21, 2026

Tags

#react
#suspense
#concurrent-rendering
#streaming-ssr
#interview

Share

Related articles