React Suspense and Concurrent Features REACT 18+

1. Suspense Component for Code Splitting

Feature Syntax Description Use Case
React.lazy() lazy(() => import('./Comp')) Dynamic import for code splitting Load components only when needed
Suspense <Suspense fallback={...}> Show fallback while lazy component loads Loading states for async components
fallback Prop fallback={<Spinner/>} Component shown during loading Skeleton screens, spinners, placeholders
Nested Suspense <Suspense>...<Suspense> Multiple suspense boundaries at different levels Granular loading states, progressive loading
Named Imports lazy(() => import().then()) Load named exports with lazy Non-default exports from modules

Example: Basic code splitting with Suspense

import { lazy, Suspense } from 'react';

// Lazy load components
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
const Settings = lazy(() => import('./Settings'));

function App() {
  return (
    <div>
      <Header /> {/* Always loaded */}
      
      <Suspense fallback={<LoadingSpinner />}>
        <Routes>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/profile" element={<Profile />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </div>
  );
}

// Named export lazy loading
const MyComponent = lazy(() =>
  import('./MyModule').then(module => ({
    default: module.MyNamedExport
  }))
);

Example: Nested Suspense boundaries for granular loading

const MainContent = lazy(() => import('./MainContent'));
const Sidebar = lazy(() => import('./Sidebar'));
const Comments = lazy(() => import('./Comments'));

function Page() {
  return (
    <div>
      {/* Top-level suspense for entire page */}
      <Suspense fallback={<PageSkeleton />}>
        <div className="layout">
          {/* Separate boundary for main content */}
          <Suspense fallback={<ContentSkeleton />}>
            <MainContent />
            
            {/* Nested boundary for comments */}
            <Suspense fallback={<div>Loading comments...</div>}>
              <Comments />
            </Suspense>
          </Suspense>
          
          {/* Separate boundary for sidebar */}
          <Suspense fallback={<SidebarSkeleton />}>
            <Sidebar />
          </Suspense>
        </div>
      </Suspense>
    </div>
  );
}

// Route-based code splitting
function App() {
  return (
    <Router>
      <Suspense fallback={<FullPageSpinner />}>
        <Routes>
          <Route path="/" element={lazy(() => import('./Home'))} />
          <Route path="/about" element={lazy(() => import('./About'))} />
        </Routes>
      </Suspense>
    </Router>
  );
}
Code Splitting Best Practices: Split at route level first, use nested Suspense for progressive loading, show meaningful fallbacks (skeletons better than spinners), preload critical routes, split large third-party libraries separately.

2. Suspense for Data Fetching Patterns

Pattern Implementation Description Status
Suspense-enabled Libraries React Query, SWR, Relay Libraries with built-in Suspense support Stable
use() Hook REACT 19 const data = use(promise) Read promise value, suspends until resolved React 19+
Resource Pattern Wrap promise in resource object Manual Suspense implementation pattern Advanced
Server Components Async components on server Native async/await in components Next.js 13+
Streaming SSR renderToPipeableStream Stream HTML with Suspense boundaries React 18+

Example: Suspense with React Query

import { useQuery } from '@tanstack/react-query';
import { Suspense } from 'react';

// Component that uses Suspense for data fetching
function UserProfile({ userId }) {
  const { data: user } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    suspense: true  // Enable Suspense mode
  });
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// Wrap with Suspense
function App() {
  return (
    <Suspense fallback={<UserSkeleton />}>
      <UserProfile userId={123} />
    </Suspense>
  );
}

// Multiple parallel data fetches with Suspense
function Dashboard() {
  return (
    <Suspense fallback={<DashboardSkeleton />}>
      <div className="dashboard">
        <Suspense fallback={<div>Loading stats...</div>}>
          <Stats />
        </Suspense>
        
        <Suspense fallback={<div>Loading activity...</div>}>
          <RecentActivity />
        </Suspense>
        
        <Suspense fallback={<div>Loading chart...</div>}>
          <Chart />
        </Suspense>
      </div>
    </Suspense>
  );
}

Example: use() hook for Suspense data fetching (React 19)

import { use, Suspense } from 'react';

// Fetch function returns a promise
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

// Component using use() hook
function UserProfile({ userPromise }) {
  // use() suspends component until promise resolves
  const user = use(userPromise);
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

// Parent component
function App() {
  // Start fetch immediately (not in component)
  const userPromise = fetchUser(123);
  
  return (
    <Suspense fallback={<LoadingSkeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

// Conditional data fetching with use()
function ConditionalData({ showData }) {
  if (!showData) {
    return <div>Enable data display</div>;
  }
  
  // use() can be called conditionally (unlike hooks!)
  const data = use(fetchData());
  return <div>{data.content}</div>;
}
Warning: Suspense for data fetching requires libraries with Suspense integration (React Query, SWR, Relay) or React 19's use() hook. Don't throw promises manually unless implementing a proper resource pattern.

3. Error Boundaries with Suspense

Concept Implementation Description Use Case
Error Boundary Wrapper Wrap Suspense with ErrorBoundary Catch errors from suspended components Handle loading failures gracefully
Separate Boundaries ErrorBoundary per Suspense Independent error handling for sections Prevent whole page failure
Retry Logic Reset error boundary state Allow user to retry failed loads Temporary network issues, transient errors
Error Fallback UI Custom error components User-friendly error messages Better UX than blank screens

Example: Error Boundary with Suspense integration

import { ErrorBoundary } from 'react-error-boundary';
import { Suspense } from 'react';

// Error fallback component with retry
function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert" className="error-container">
      <h2>Something went wrong</h2>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>
        Try again
      </button>
    </div>
  );
}

// Combined Error + Suspense boundary
function Page() {
  return (
    <ErrorBoundary 
      FallbackComponent={ErrorFallback}
      onReset={() => {
        // Reset any cached data
        queryClient.resetQueries();
      }}
    >
      <Suspense fallback={<LoadingSkeleton />}>
        <UserProfile />
      </Suspense>
    </ErrorBoundary>
  );
}

// Multiple boundaries for granular error handling
function Dashboard() {
  return (
    <div className="dashboard">
      {/* Each section has its own error boundary */}
      <ErrorBoundary fallback={<SectionError section="Stats" />}>
        <Suspense fallback={<StatsSkeleton />}>
          <Stats />
        </Suspense>
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<SectionError section="Activity" />}>
        <Suspense fallback={<ActivitySkeleton />}>
          <RecentActivity />
        </Suspense>
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<SectionError section="Chart" />}>
        <Suspense fallback={<ChartSkeleton />}>
          <Chart />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

Example: Reusable boundary component

// Combined ErrorBoundary + Suspense component
function AsyncBoundary({ 
  children, 
  fallback, 
  errorFallback,
  onReset 
}) {
  return (
    <ErrorBoundary 
      fallbackRender={({ error, resetErrorBoundary }) => 
        errorFallback ? 
          errorFallback(error, resetErrorBoundary) : 
          <DefaultErrorFallback error={error} reset={resetErrorBoundary} />
      }
      onReset={onReset}
    >
      <Suspense fallback={fallback || <DefaultLoadingFallback />}>
        {children}
      </Suspense>
    </ErrorBoundary>
  );
}

// Usage - clean and simple
function App() {
  return (
    <AsyncBoundary
      fallback={<PageSkeleton />}
      errorFallback={(error, reset) => (
        <PageError error={error} onRetry={reset} />
      )}
    >
      <UserDashboard />
    </AsyncBoundary>
  );
}
Error Boundary + Suspense Pattern: Always wrap Suspense with ErrorBoundary, use granular boundaries to prevent cascade failures, implement retry logic, show helpful error messages, log errors to monitoring services.

4. Concurrent Rendering and Time Slicing

Feature Description Benefit Availability
Concurrent Mode React can interrupt, pause, resume rendering Keeps app responsive during expensive renders React 18+
Time Slicing Break rendering work into chunks Yield to browser between chunks for interactions Automatic with React 18
Automatic Batching Batch multiple state updates together Fewer re-renders, better performance Enabled by default React 18
Interruptible Rendering React can pause low-priority work High-priority updates don't get blocked With concurrent features
Priority Levels Urgent vs non-urgent updates Keep UI responsive for user input useTransition, useDeferredValue

Example: Concurrent rendering with createRoot

import { createRoot } from 'react-dom/client';

// React 18+ concurrent mode (default with createRoot)
const root = createRoot(document.getElementById('root'));
root.render(<App />);

// Automatic batching in React 18
function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  
  function handleClick() {
    // React 18 batches these together automatically
    // Even in async callbacks, timeouts, native events!
    setCount(c => c + 1);
    setFlag(f => !f);
    // Only 1 re-render instead of 2
  }
  
  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? 'blue' : 'black' }}>
        {count}
      </h1>
    </div>
  );
}

// Opt-out of batching if needed (rare)
import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCount(c => c + 1);
  });
  // DOM updated immediately
  
  flushSync(() => {
    setFlag(f => !f);
  });
  // DOM updated again - 2 separate renders
}

Example: Visualizing concurrent rendering benefits

// Without concurrent features (React 17)
// Long render blocks everything
function HeavyList({ items }) {
  // This blocks the entire UI during render
  return (
    <ul>
      {items.map(item => (
        <ExpensiveItem key={item.id} item={item} />
      ))}
    </ul>
  );
}
// User can't type or interact until render completes

// With concurrent features (React 18+)
function ResponsiveList({ items }) {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  const [displayItems, setDisplayItems] = useState(items);
  
  function handleSearch(e) {
    const value = e.target.value;
    setQuery(value); // Urgent - updates immediately
    
    // Non-urgent - can be interrupted
    startTransition(() => {
      const filtered = items.filter(item =>
        item.name.includes(value)
      );
      setDisplayItems(filtered);
    });
  }
  
  return (
    <div>
      <input value={query} onChange={handleSearch} />
      {isPending && <Spinner />}
      <ul className={isPending ? 'loading' : ''}>
        {displayItems.map(item => (
          <ExpensiveItem key={item.id} item={item} />
        ))}
      </ul>
    </div>
  );
}
// Input stays responsive - filtering happens in background
Concurrent Features: Enabled by default with createRoot in React 18, automatic batching improves performance automatically, time slicing keeps UI responsive, use transitions for non-urgent updates, no breaking changes for existing code.

5. useTransition for State Transitions

Feature Syntax Description Use Case
useTransition Hook const [isPending, start] = useTransition() Mark state updates as non-urgent transitions Keep UI responsive during expensive updates
isPending Flag isPending Boolean indicating if transition is in progress Show loading indicators, disable UI temporarily
startTransition startTransition(() => {...}) Function to wrap non-urgent state updates Large list filters, tab switches, searches
Priority Lower than user input Can be interrupted by urgent updates Heavy renders don't block interactions
import { useState, useTransition } from 'react';

function SearchResults() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();
  
  function handleSearch(e) {
    const value = e.target.value;
    
    // Urgent: Update input immediately
    setQuery(value);
    
    // Non-urgent: Update results (can be interrupted)
    startTransition(() => {
      // Expensive filtering
      const filtered = allItems.filter(item =>
        item.title.toLowerCase().includes(value.toLowerCase()) ||
        item.description.toLowerCase().includes(value.toLowerCase())
      );
      setResults(filtered);
    });
  }
  
  return (
    <div>
      <input 
        type="text"
        value={query}
        onChange={handleSearch}
        placeholder="Search..."
      />
      
      {isPending && <LoadingSpinner />}
      
      <ul className={isPending ? 'opacity-50' : ''}>
        {results.map(item => (
          <SearchResultItem key={item.id} item={item} />
        ))}
      </ul>
    </div>
  );
}

Example: Tab switching with transitions

function Tabs() {
  const [activeTab, setActiveTab] = useState('home');
  const [isPending, startTransition] = useTransition();
  
  function handleTabClick(tab) {
    // Mark tab content update as transition
    startTransition(() => {
      setActiveTab(tab);
    });
  }
  
  return (
    <div>
      <div className="tab-buttons">
        <button 
          onClick={() => handleTabClick('home')}
          className={activeTab === 'home' ? 'active' : ''}
        >
          Home
        </button>
        <button 
          onClick={() => handleTabClick('posts')}
          className={activeTab === 'posts' ? 'active' : ''}
        >
          Posts {isPending && '...'}
        </button>
        <button 
          onClick={() => handleTabClick('contact')}
          className={activeTab === 'contact' ? 'active' : ''}
        >
          Contact
        </button>
      </div>
      
      <div className={`tab-content ${isPending ? 'loading' : ''}`}>
        {activeTab === 'home' && <HomeTab />}
        {activeTab === 'posts' && <PostsTab />} {/* Expensive */}
        {activeTab === 'contact' && <ContactTab />}
      </div>
    </div>
  );
}

// Comparison: without useTransition
function SlowTabs() {
  const [activeTab, setActiveTab] = useState('home');
  
  // Tab clicks feel laggy because UI waits for expensive render
  return (
    <div>
      <button onClick={() => setActiveTab('posts')}>
        Posts
      </button>
      {/* UI freezes until PostsTab renders */}
      {activeTab === 'posts' && <PostsTab />}
    </div>
  );
}

Example: Pagination with transitions

function PaginatedList() {
  const [page, setPage] = useState(1);
  const [isPending, startTransition] = useTransition();
  
  // Fetch data for current page
  const { data, loading } = useFetch(`/api/items?page=${page}`);
  
  function goToPage(newPage) {
    startTransition(() => {
      setPage(newPage);
    });
  }
  
  return (
    <div>
      {/* Show current data while transitioning */}
      <div className={isPending ? 'transitioning' : ''}>
        {loading ? (
          <Spinner />
        ) : (
          <ItemList items={data} />
        )}
      </div>
      
      <div className="pagination">
        <button 
          onClick={() => goToPage(page - 1)}
          disabled={page === 1 || isPending}
        >
          Previous
        </button>
        
        <span>Page {page} {isPending && '(loading...)'}</span>
        
        <button 
          onClick={() => goToPage(page + 1)}
          disabled={isPending}
        >
          Next
        </button>
      </div>
    </div>
  );
}
useTransition Best Practices: Use for expensive updates that aren't time-sensitive, keep user input responsive (don't wrap input onChange), show isPending state to user, works with Suspense boundaries, test on low-end devices to see benefits.

6. Server Suspense and Streaming SSR

Feature API Description Use Case
Streaming SSR renderToPipeableStream() Stream HTML to client progressively Faster initial page load, better TTFB
Selective Hydration Automatic with Suspense Hydrate critical parts first Interactive faster, better TTI
Server Suspense <Suspense> on server Show fallback while server fetches data Stream page with loading states
Server Components Async components (Next.js) Await data directly in components Zero-bundle data fetching
Progressive Loading Nested Suspense boundaries Stream page sections independently Show content as it's ready

Example: Node.js streaming SSR setup

import { renderToPipeableStream } from 'react-dom/server';

// Server-side streaming setup
app.get('/', (req, res) => {
  const { pipe, abort } = renderToPipeableStream(
    <App />,
    {
      bootstrapScripts: ['/main.js'],
      onShellReady() {
        // Start streaming immediately
        res.setHeader('Content-Type', 'text/html');
        pipe(res);
      },
      onShellError(error) {
        res.status(500).send('<h1>Server Error</h1>');
      },
      onAllReady() {
        // All Suspense boundaries resolved
        console.log('All content ready');
      },
      onError(error) {
        console.error('Stream error:', error);
      }
    }
  );
  
  // Abort after timeout
  setTimeout(() => abort(), 10000);
});

// App with Suspense boundaries
function App() {
  return (
    <html>
      <head>
        <title>Streaming SSR App</title>
      </head>
      <body>
        <Header /> {/* Rendered immediately */}
        
        {/* Stream this section separately */}
        <Suspense fallback={<CommentsSkeleton />}>
          <Comments /> {/* Slow data fetch */}
        </Suspense>
        
        <Footer /> {/* Rendered immediately */}
      </body>
    </html>
  );
}

Example: Next.js Server Components with Suspense

// app/page.tsx (Next.js 13+ App Router)
import { Suspense } from 'react';

// Server Component - can be async!
async function UserProfile({ userId }) {
  // Fetch directly in component (server-side)
  const user = await fetch(`https://api.example.com/users/${userId}`)
    .then(res => res.json());
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

async function Posts() {
  const posts = await fetch('https://api.example.com/posts')
    .then(res => res.json());
  
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

// Page with streaming sections
export default function Page() {
  return (
    <div>
      <h1>My Page</h1>
      
      {/* Profile streams first */}
      <Suspense fallback={<ProfileSkeleton />}>
        <UserProfile userId="123" />
      </Suspense>
      
      {/* Posts stream independently */}
      <Suspense fallback={<PostsSkeleton />}>
        <Posts />
      </Suspense>
    </div>
  );
}
// HTML streams progressively as data arrives

Example: Selective hydration benefits

// Page with multiple sections
function App() {
  return (
    <div>
      <Header /> {/* Hydrates first (small, fast) */}
      
      {/* Heavy component wrapped in Suspense */}
      <Suspense fallback={<ChartSkeleton />}>
        <HeavyChart /> {/* Hydrates later, doesn't block */}
      </Suspense>
      
      {/* User can interact with button before chart hydrates */}
      <button onClick={handleClick}>
        Click me (interactive immediately!)
      </button>
      
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments /> {/* Hydrates after chart */}
      </Suspense>
    </div>
  );
}

// Benefits of selective hydration:
// 1. Faster Time to Interactive (TTI)
// 2. User can interact with parts before full hydration
// 3. Heavy components don't block critical interactions
// 4. Automatic priority based on user interaction
Streaming SSR Benefits: Faster TTFB (Time to First Byte), progressive page rendering, selective hydration for better TTI, better perceived performance, works with Suspense boundaries, built into Next.js 13+ App Router.

Suspense and Concurrent Features Best Practices