Overview

React’s concurrent features enable better user experiences by allowing React to interrupt rendering work to handle higher-priority updates. These features are particularly valuable in Next.js applications for creating responsive interfaces.

What are Concurrent Features?

Concurrent features allow React to work on multiple tasks simultaneously and prioritize urgent updates over less critical ones.

Concurrent Benefits

  • Better responsiveness
  • Improved user experience
  • Non-blocking updates
  • Priority-based rendering

Concurrent Features

  • Automatic batching
  • Transitions
  • Suspense improvements
  • Concurrent rendering

Automatic Batching

React 18 automatically batches state updates, reducing the number of re-renders and improving performance.

Before React 18

// Multiple re-renders
function handleClick() {
  setCount((c) => c + 1); // Re-render 1
  setFlag((f) => !f); // Re-render 2
  setData((d) => d + 1); // Re-render 3
}

After React 18

// Single re-render (automatic batching)
function handleClick() {
  setCount((c) => c + 1); // Batched
  setFlag((f) => !f); // Batched
  setData((d) => d + 1); // Batched
  // Only one re-render
}

Batching in Event Handlers

"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  const handleClick = () => {
    // These updates are automatically batched
    setCount((c) => c + 1);
    setFlag((f) => !f);

    // Only one re-render will occur
    console.log("Component will re-render once");
  };

  return (
    <div>
      <p>Count: {count}</p>
      <p>Flag: {flag ? "true" : "false"}</p>
      <button onClick={handleClick}>Update Both</button>
    </div>
  );
}

Transitions

Transitions allow you to mark state updates as non-urgent, enabling React to interrupt them for more important updates.

Basic Transitions

"use client";

import { useState, useTransition } from "react";

export default function SearchResults() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (newQuery: string) => {
    setQuery(newQuery); // Urgent update

    startTransition(() => {
      // Non-urgent update
      setResults(expensiveSearch(newQuery));
    });
  };

  return (
    <div>
      <input
        value={query}
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="Search..."
      />

      {isPending && <div>Searching...</div>}

      <div className="results">
        {results.map((result) => (
          <div key={result.id}>{result.title}</div>
        ))}
      </div>
    </div>
  );
}

Transitions with useDeferredValue

"use client";

import { useState, useDeferredValue, useMemo } from "react";

export default function ProductList() {
  const [query, setQuery] = useState("");
  const deferredQuery = useDeferredValue(query);

  const products = useMemo(() => {
    return expensiveProductSearch(deferredQuery);
  }, [deferredQuery]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search products..."
      />

      <div className="products">
        {products.map((product) => (
          <div key={product.id} className="product">
            <h3>{product.name}</h3>
            <p>{product.description}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

Concurrent Rendering

Interruptible Rendering

"use client";

import { useState, useTransition } from "react";

export default function DataTable() {
  const [data, setData] = useState([]);
  const [isPending, startTransition] = useTransition();
  const [filter, setFilter] = useState("");

  const handleFilterChange = (newFilter: string) => {
    setFilter(newFilter); // Urgent update

    startTransition(() => {
      // Non-urgent update - can be interrupted
      const filteredData = expensiveFilter(data, newFilter);
      setData(filteredData);
    });
  };

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => handleFilterChange(e.target.value)}
        placeholder="Filter data..."
      />

      {isPending && <div>Filtering...</div>}

      <table>
        <tbody>
          {data.map((row) => (
            <tr key={row.id}>
              <td>{row.name}</td>
              <td>{row.value}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Advanced Concurrent Patterns

Priority Updates

"use client";

import { useState, useTransition, useDeferredValue } from "react";

export default function Dashboard() {
  const [urgentData, setUrgentData] = useState(null);
  const [heavyData, setHeavyData] = useState(null);
  const [isPending, startTransition] = useTransition();
  const deferredHeavyData = useDeferredValue(heavyData);

  const handleUrgentUpdate = () => {
    // This update has high priority
    setUrgentData(fetchUrgentData());
  };

  const handleHeavyUpdate = () => {
    // This update has low priority
    startTransition(() => {
      setHeavyData(expensiveCalculation());
    });
  };

  return (
    <div className="dashboard">
      <div className="urgent-section">
        <h2>Urgent Updates</h2>
        <button onClick={handleUrgentUpdate}>Update Urgent Data</button>
        {urgentData && <div>{urgentData}</div>}
      </div>

      <div className="heavy-section">
        <h2>Heavy Updates</h2>
        <button onClick={handleHeavyUpdate}>Update Heavy Data</button>
        {isPending && <div>Processing...</div>}
        {deferredHeavyData && <div>{deferredHeavyData}</div>}
      </div>
    </div>
  );
}

Concurrent Data Fetching

"use client";

import { useState, useTransition, useDeferredValue } from "react";

export default function SearchInterface() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();
  const deferredQuery = useDeferredValue(query);

  const handleSearch = async (searchQuery: string) => {
    startTransition(async () => {
      const searchResults = await fetchSearchResults(searchQuery);
      setResults(searchResults);
    });
  };

  return (
    <div>
      <input
        value={query}
        onChange={(e) => {
          setQuery(e.target.value);
          handleSearch(e.target.value);
        }}
        placeholder="Search..."
      />

      {isPending && <div>Searching...</div>}

      <div className="results">
        {results.map((result) => (
          <div key={result.id} className="result">
            <h3>{result.title}</h3>
            <p>{result.description}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

Performance Optimization

Optimizing Heavy Components

"use client";

import { useState, useTransition, useDeferredValue, useMemo } from "react";

export default function LargeDataTable() {
  const [data, setData] = useState([]);
  const [filter, setFilter] = useState("");
  const [sortBy, setSortBy] = useState("name");
  const [isPending, startTransition] = useTransition();

  const deferredFilter = useDeferredValue(filter);
  const deferredSortBy = useDeferredValue(sortBy);

  const processedData = useMemo(() => {
    return expensiveDataProcessing(data, deferredFilter, deferredSortBy);
  }, [data, deferredFilter, deferredSortBy]);

  const handleFilterChange = (newFilter: string) => {
    setFilter(newFilter);
  };

  const handleSortChange = (newSortBy: string) => {
    setSortBy(newSortBy);
  };

  return (
    <div>
      <div className="controls">
        <input
          value={filter}
          onChange={(e) => handleFilterChange(e.target.value)}
          placeholder="Filter..."
        />
        <select
          value={sortBy}
          onChange={(e) => handleSortChange(e.target.value)}>
          <option value="name">Name</option>
          <option value="date">Date</option>
          <option value="value">Value</option>
        </select>
      </div>

      {isPending && <div>Processing data...</div>}

      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Date</th>
            <th>Value</th>
          </tr>
        </thead>
        <tbody>
          {processedData.map((row) => (
            <tr key={row.id}>
              <td>{row.name}</td>
              <td>{row.date}</td>
              <td>{row.value}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Real-World Examples

Search with Debouncing

"use client";

import { useState, useTransition, useDeferredValue, useEffect } from "react";

export default function SearchPage() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();
  const deferredQuery = useDeferredValue(query);

  useEffect(() => {
    if (deferredQuery) {
      startTransition(async () => {
        const searchResults = await searchAPI(deferredQuery);
        setResults(searchResults);
      });
    }
  }, [deferredQuery]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />

      {isPending && <div>Searching...</div>}

      <div className="results">
        {results.map((result) => (
          <div key={result.id} className="result">
            <h3>{result.title}</h3>
            <p>{result.description}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

Form with Heavy Validation

"use client";

import { useState, useTransition, useDeferredValue } from "react";

export default function ContactForm() {
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    message: "",
  });
  const [validation, setValidation] = useState({});
  const [isPending, startTransition] = useTransition();

  const deferredFormData = useDeferredValue(formData);

  const handleInputChange = (field: string, value: string) => {
    setFormData((prev) => ({ ...prev, [field]: value }));

    // Heavy validation in transition
    startTransition(() => {
      const validationResult = expensiveValidation(deferredFormData);
      setValidation(validationResult);
    });
  };

  return (
    <form>
      <div>
        <input
          type="text"
          value={formData.name}
          onChange={(e) => handleInputChange("name", e.target.value)}
          placeholder="Name"
        />
        {validation.name && <span className="error">{validation.name}</span>}
      </div>

      <div>
        <input
          type="email"
          value={formData.email}
          onChange={(e) => handleInputChange("email", e.target.value)}
          placeholder="Email"
        />
        {validation.email && <span className="error">{validation.email}</span>}
      </div>

      <div>
        <textarea
          value={formData.message}
          onChange={(e) => handleInputChange("message", e.target.value)}
          placeholder="Message"
        />
        {validation.message && (
          <span className="error">{validation.message}</span>
        )}
      </div>

      {isPending && <div>Validating...</div>}

      <button type="submit">Submit</button>
    </form>
  );
}

Best Practices

Concurrent Feature Usage

1

Identify Heavy Operations

Find operations that can block the UI
2

Use Transitions

Wrap heavy operations in startTransition
3

Defer Values

Use useDeferredValue for expensive computations
4

Provide Feedback

Show loading states during transitions

Performance Considerations

Batching Benefits

  • Reduces re-renders
  • Improves performance
  • Automatic in React 18
  • Works in event handlers

Transition Benefits

  • Non-blocking updates
  • Better responsiveness
  • Priority-based rendering
  • Improved user experience

Common Patterns

Progressive Loading

"use client";

import { useState, useTransition } from "react";

export default function ProgressiveLoader() {
  const [data, setData] = useState([]);
  const [isPending, startTransition] = useTransition();

  const loadData = () => {
    startTransition(() => {
      // Load data progressively
      const newData = generateLargeDataset();
      setData(newData);
    });
  };

  return (
    <div>
      <button onClick={loadData}>Load Data</button>

      {isPending && <div>Loading data...</div>}

      <div className="data-grid">
        {data.map((item) => (
          <div key={item.id} className="data-item">
            {item.content}
          </div>
        ))}
      </div>
    </div>
  );
}

Responsive Updates

"use client";

import { useState, useTransition, useDeferredValue } from "react";

export default function ResponsiveInterface() {
  const [input, setInput] = useState("");
  const [output, setOutput] = useState("");
  const [isPending, startTransition] = useTransition();

  const deferredInput = useDeferredValue(input);

  const processInput = (value: string) => {
    startTransition(() => {
      const processed = expensiveProcessing(value);
      setOutput(processed);
    });
  };

  return (
    <div>
      <input
        value={input}
        onChange={(e) => {
          setInput(e.target.value);
          processInput(e.target.value);
        }}
        placeholder="Type something..."
      />

      {isPending && <div>Processing...</div>}

      <div className="output">{output}</div>
    </div>
  );
}

Troubleshooting

Key Takeaway: Concurrent features enable React to create more responsive user interfaces by prioritizing urgent updates and allowing non-urgent work to be interrupted. Use them to improve perceived performance and user experience.

Overview

React’s streaming and Suspense features enable progressive rendering and better loading experiences. These features are particularly powerful in Next.js applications where they can significantly improve perceived performance.

What is Streaming?

Streaming allows React to send HTML to the browser as it’s being rendered on the server, rather than waiting for the entire page to be ready.

Streaming Benefits

  • Faster initial page load
  • Progressive content rendering
  • Better perceived performance
  • Reduced time to first byte

Streaming Use Cases

  • Slow data fetching
  • Large component trees
  • Third-party integrations
  • Complex calculations

Understanding Suspense

Suspense is a React component that lets you declaratively handle loading states for components that are waiting for data.

Basic Suspense Usage

import { Suspense } from "react";

function App() {
  return (
    <div>
      <h1>My App</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}

Suspense with Custom Loading

import { Suspense } from "react";
import LoadingSpinner from "@/components/LoadingSpinner";

function Dashboard() {
  return (
    <div className="dashboard">
      <h1>Dashboard</h1>
      <Suspense fallback={<LoadingSpinner />}>
        <DashboardContent />
      </Suspense>
    </div>
  );
}

Streaming in Next.js

Automatic Streaming

Next.js automatically streams server components by default:
// app/dashboard/page.tsx
import { db } from "@/lib/database";

export default async function Dashboard() {
  // This will stream as it loads
  const stats = await db.getDashboardStats();

  return (
    <div>
      <h1>Dashboard</h1>
      <div className="stats">
        {stats.map((stat) => (
          <div key={stat.id}>
            {stat.title}: {stat.value}
          </div>
        ))}
      </div>
    </div>
  );
}

Manual Streaming with Suspense

// app/dashboard/page.tsx
import { Suspense } from "react";
import DashboardStats from "@/components/DashboardStats";
import DashboardCharts from "@/components/DashboardCharts";

export default function Dashboard() {
  return (
    <div className="dashboard">
      <h1>Dashboard</h1>

      <Suspense fallback={<StatsSkeleton />}>
        <DashboardStats />
      </Suspense>

      <Suspense fallback={<ChartsSkeleton />}>
        <DashboardCharts />
      </Suspense>
    </div>
  );
}

Advanced Streaming Patterns

Nested Suspense Boundaries

// app/dashboard/page.tsx
import { Suspense } from "react";

export default function Dashboard() {
  return (
    <div className="dashboard">
      <h1>Dashboard</h1>

      <Suspense fallback={<div>Loading stats...</div>}>
        <StatsSection />
      </Suspense>

      <Suspense fallback={<div>Loading charts...</div>}>
        <ChartsSection />
      </Suspense>

      <Suspense fallback={<div>Loading recent activity...</div>}>
        <RecentActivity />
      </Suspense>
    </div>
  );
}

// Each section can have its own Suspense boundaries
function StatsSection() {
  return (
    <div className="stats-section">
      <Suspense fallback={<div>Loading user stats...</div>}>
        <UserStats />
      </Suspense>

      <Suspense fallback={<div>Loading system stats...</div>}>
        <SystemStats />
      </Suspense>
    </div>
  );
}

Streaming with Error Boundaries

// app/dashboard/page.tsx
import { Suspense } from "react";
import ErrorBoundary from "@/components/ErrorBoundary";

export default function Dashboard() {
  return (
    <div className="dashboard">
      <h1>Dashboard</h1>

      <ErrorBoundary fallback={<StatsError />}>
        <Suspense fallback={<StatsSkeleton />}>
          <DashboardStats />
        </Suspense>
      </ErrorBoundary>

      <ErrorBoundary fallback={<ChartsError />}>
        <Suspense fallback={<ChartsSkeleton />}>
          <DashboardCharts />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

Data Fetching with Suspense

Server Components with Suspense

// app/blog/page.tsx
import { Suspense } from "react";
import { db } from "@/lib/database";

async function BlogPosts() {
  const posts = await db.getBlogPosts();

  return (
    <div className="blog-posts">
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

async function BlogSidebar() {
  const categories = await db.getCategories();

  return (
    <aside className="blog-sidebar">
      <h3>Categories</h3>
      <ul className="list-disc list-inside">
        {categories.map((category) => (
          <li key={category.id}>
            <a href={`/blog/category/${category.slug}`}>{category.name}</a>
          </li>
        ))}
      </ul>
    </aside>
  );
}

export default function BlogPage() {
  return (
    <div className="blog-layout">
      <main>
        <Suspense fallback={<PostsSkeleton />}>
          <BlogPosts />
        </Suspense>
      </main>

      <aside>
        <Suspense fallback={<SidebarSkeleton />}>
          <BlogSidebar />
        </Suspense>
      </aside>
    </div>
  );
}

Client Components with Suspense

"use client";

import { Suspense, useState } from "react";

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);

  // Simulate data fetching
  useState(() => {
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then(setUser);
  }, [userId]);

  if (!user) {
    throw new Promise((resolve) => setTimeout(resolve, 1000));
  }

  return (
    <div className="user-profile">
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

export default function ProfilePage() {
  return (
    <div>
      <h1>Profile</h1>
      <Suspense fallback={<ProfileSkeleton />}>
        <UserProfile userId="123" />
      </Suspense>
    </div>
  );
}

Performance Optimization

Streaming Strategies

1

Critical Path First

Stream the most important content first
2

Progressive Enhancement

Load basic content first, then enhance
3

Parallel Loading

Load multiple sections simultaneously
4

Error Isolation

Use error boundaries to prevent cascading failures

Loading State Design

// components/StatsSkeleton.tsx
export default function StatsSkeleton() {
  return (
    <div className="stats-skeleton">
      <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
        {Array.from({ length: 4 }).map((_, i) => (
          <div key={i} className="bg-white p-6 rounded-lg shadow-md">
            <div className="h-4 bg-gray-200 rounded w-24 mb-2 animate-pulse"></div>
            <div className="h-8 bg-gray-200 rounded w-16 animate-pulse"></div>
          </div>
        ))}
      </div>
    </div>
  );
}

Real-World Examples

E-commerce Product Page

// app/products/[id]/page.tsx
import { Suspense } from "react";

async function ProductInfo({ productId }: { productId: string }) {
  const product = await db.getProduct(productId);

  return (
    <div className="product-info">
      <h1>{product.name}</h1>
      <p className="price">${product.price}</p>
      <p>{product.description}</p>
    </div>
  );
}

async function ProductReviews({ productId }: { productId: string }) {
  const reviews = await db.getProductReviews(productId);

  return (
    <div className="product-reviews">
      <h2>Reviews</h2>
      {reviews.map((review) => (
        <div key={review.id} className="review">
          <h3>{review.title}</h3>
          <p>{review.content}</p>
        </div>
      ))}
    </div>
  );
}

async function RelatedProducts({ productId }: { productId: string }) {
  const related = await db.getRelatedProducts(productId);

  return (
    <div className="related-products">
      <h2>Related Products</h2>
      <div className="grid grid-cols-4 gap-4">
        {related.map((product) => (
          <div key={product.id} className="product-card">
            <h3>{product.name}</h3>
            <p>${product.price}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

export default function ProductPage({ params }: { params: { id: string } }) {
  return (
    <div className="product-page">
      <Suspense fallback={<ProductInfoSkeleton />}>
        <ProductInfo productId={params.id} />
      </Suspense>

      <div className="product-details">
        <Suspense fallback={<ReviewsSkeleton />}>
          <ProductReviews productId={params.id} />
        </Suspense>

        <Suspense fallback={<RelatedSkeleton />}>
          <RelatedProducts productId={params.id} />
        </Suspense>
      </div>
    </div>
  );
}

Dashboard with Multiple Data Sources

// app/dashboard/page.tsx
import { Suspense } from "react";

async function UserStats() {
  const stats = await db.getUserStats();
  return <StatsWidget data={stats} />;
}

async function SystemMetrics() {
  const metrics = await db.getSystemMetrics();
  return <MetricsWidget data={metrics} />;
}

async function RecentActivity() {
  const activity = await db.getRecentActivity();
  return <ActivityFeed data={activity} />;
}

export default function Dashboard() {
  return (
    <div className="dashboard">
      <h1>Dashboard</h1>

      <div className="dashboard-grid">
        <div className="stats-section">
          <Suspense fallback={<StatsSkeleton />}>
            <UserStats />
          </Suspense>
        </div>

        <div className="metrics-section">
          <Suspense fallback={<MetricsSkeleton />}>
            <SystemMetrics />
          </Suspense>
        </div>

        <div className="activity-section">
          <Suspense fallback={<ActivitySkeleton />}>
            <RecentActivity />
          </Suspense>
        </div>
      </div>
    </div>
  );
}

Best Practices

Streaming Best Practices

1

Identify Slow Components

Find components that take time to load
2

Create Loading States

Design appropriate loading skeletons
3

Use Error Boundaries

Handle errors gracefully in streaming
4

Monitor Performance

Track streaming performance metrics

Suspense Best Practices

Granular Boundaries

  • Use multiple Suspense boundaries
  • Isolate slow components
  • Provide specific loading states

Error Handling

  • Combine with error boundaries
  • Provide fallback content
  • Handle network failures

Common Patterns

Progressive Loading

// app/dashboard/page.tsx
export default function Dashboard() {
  return (
    <div className="dashboard">
      <h1>Dashboard</h1>

      {/* Load immediately */}
      <QuickStats />

      {/* Load progressively */}
      <Suspense fallback={<div>Loading detailed stats...</div>}>
        <DetailedStats />
      </Suspense>

      <Suspense fallback={<div>Loading charts...</div>}>
        <Charts />
      </Suspense>
    </div>
  );
}

Conditional Streaming

// app/dashboard/page.tsx
import { Suspense } from "react";

export default function Dashboard({
  searchParams,
}: {
  searchParams: { tab?: string };
}) {
  const activeTab = searchParams.tab || "overview";

  return (
    <div className="dashboard">
      <h1>Dashboard</h1>

      {activeTab === "overview" && (
        <Suspense fallback={<OverviewSkeleton />}>
          <OverviewTab />
        </Suspense>
      )}

      {activeTab === "analytics" && (
        <Suspense fallback={<AnalyticsSkeleton />}>
          <AnalyticsTab />
        </Suspense>
      )}
    </div>
  );
}

Troubleshooting

Key Takeaway: Streaming and Suspense are powerful tools for creating better user experiences. Use them to progressively load content and provide immediate feedback to users while data is being fetched.