React 19 Suspense i Concurrent Rendering: Streaming SSR oraz pytania rekrutacyjne 2026

Kompleksowy przewodnik po React 19 Suspense, concurrent rendering i streaming SSR. Poznaj use() API, useTransition, useDeferredValue oraz przygotuj się do rozmów rekrutacyjnych.

React 19 Suspense i Concurrent Rendering diagram

React 19 Suspense fundamentalnie zmienia sposób, w jaki aplikacje obsługują operacje asynchroniczne. Dostarcza deklaratywny model zarządzania stanami ładowania, obsługi błędów oraz pobierania danych. W połączeniu z concurrent rendering, Suspense umożliwia streaming SSR, który przyspiesza czas wyświetlenia treści, jednocześnie utrzymując responsywność interfejsu podczas intensywnych obliczeń.

Kluczowe informacje

Granice Suspense definiują, gdzie pojawiają się fallbacki ładowania. Concurrent rendering pozwala React przerywać niskopriorytowe zadania, aby zachować responsywność interakcji o wysokim priorytecie (pisanie, klikanie). Razem umożliwiają progresywną hydrację i streamowanie HTML z serwera.

Różnice między Concurrent Rendering a synchronicznym React

Przed React 18 renderowanie było synchroniczne: gdy React rozpoczął renderowanie drzewa komponentów, musiał je ukończyć przed obsługą jakiejkolwiek innej pracy. Concurrent rendering łamie ten model, pozwalając React wstrzymywać, przerywać i wznawiać renderowanie w oparciu o priorytety.

Dokumentacja React opisuje concurrent rendering jako mechanizm działający w tle, który umożliwia funkcje takie jak Suspense, transitions i streaming SSR. Kluczowa idea: React może przygotowywać wiele wersji UI jednocześnie.

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

function SearchResults({ query }: { query: string }) {
  // Kosztowne obliczenia lub pobieranie danych
  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;
    // Wysoki priorytet: natychmiastowa aktualizacja inputa
    setQuery(value);
    // Niski priorytet: aktualizacja wyników w tle
    startTransition(() => {
      setDeferredQuery(value);
    });
  }

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

Hook useTransition oznacza aktualizacje stanu jako niepilne. React kontynuuje wyświetlanie poprzedniego UI podczas obliczania nowego w tle, a następnie podmienia je, gdy jest gotowe.

Granice Suspense i hierarchia ładowania

Granice Suspense definiują fallbackowe UI dla komponentów, które się zawieszają. Gdy komponent wewnątrz granicy Suspense rzuca Promise (sygnalizując, że dane się ładują), React wyświetla fallback do momentu rozwiązania Promise.

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

function ProductDetails({ id }: { id: string }) {
  // Ten komponent zawiesza się podczas pobierania
  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>
      {/* Zewnętrzna granica: łapie oba jeśli potrzeba */}
      <Suspense fallback={<PageSkeleton />}>
        {/* Wewnętrzna granica: recenzje mogą się ładować niezależnie */}
        <ProductDetails id={id} />
        <Suspense fallback={<ReviewsSkeleton />}>
          <ProductReviews id={id} />
        </Suspense>
      </Suspense>
    </div>
  );
}

Zagnieżdżone granice Suspense umożliwiają granularną kontrolę stanów ładowania. Szczegóły produktu mogą się renderować zaraz po załadowaniu, podczas gdy recenzje pokazują własny szkielet.

API use() do pobierania danych

React 19 wprowadza API use(), udokumentowane w React RFC, jako oficjalny sposób odczytywania Promise i kontekstu podczas renderowania. W przeciwieństwie do hooków, use() może być wywoływane warunkowo.

UserProfile.tsxtsx
import { use, Suspense } from 'react';

// Promise utworzony poza komponentem (render-as-you-fetch)
const userPromise = fetchUser(userId);

function UserProfile() {
  // use() zawiesza się do rozwiązania Promise
  const user = use(userPromise);
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

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

Wzorzec render-as-you-fetch rozpoczyna pobieranie danych przed rozpoczęciem renderowania, typowo w loaderach tras lub komponentach serwerowych. Zachowuje to równoległość, ponieważ wiele pobrań może działać jednocześnie zamiast kaskadowo.

Gotowy na rozmowy o React / Next.js?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Streaming SSR z React Server Components

Streaming SSR wysyła HTML do klienta w fragmentach, gdy dane stają się dostępne. W połączeniu z React Server Components, to podejście zapewnia szybsze pierwsze wyświetlenie treści przy progresywnej hydracji interaktywnych części.

app/products/[id]/page.tsx (Next.js App Router)tsx
import { Suspense } from 'react';

// Server Component: działa tylko na serwerze
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 z wolnymi danymi
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>
      {/* Streamuje natychmiast */}
      <ProductInfo id={params.id} />
      
      {/* Streamuje gdy gotowe, najpierw pokazuje szkielet */}
      <Suspense fallback={<GridSkeleton />}>
        <RelatedProducts categoryId={product.categoryId} />
      </Suspense>
    </main>
  );
}

Serwer najpierw streamuje HTML ProductInfo. Gdy dane RelatedProducts są gotowe, React wysyła dodatkowy HTML z instrukcjami zastąpienia szkieletu.

useDeferredValue dla kosztownych obliczeń

Podczas gdy useTransition opakowuje aktualizacje stanu, useDeferredValue opóźnia renderowanie konkretnej wartości. Sprawdza się przy filtrowaniu dużych list lub kosztownych obliczeniach pochodnych.

FilterableList.tsxtsx
import { useState, useDeferredValue, useMemo } from 'react';

function ExpensiveList({ filter }: { filter: string }) {
  // Kosztowna operacja filtrowania
  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('');
  // Opóźniona wersja pozostaje w tyle podczas pisania
  const deferredFilter = useDeferredValue(filter);
  const isStale = filter !== deferredFilter;

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

Input pozostaje responsywny, ponieważ React priorytetyzuje aktualizację stanu. Lista renderuje się z opóźnioną wartością, gdy React ma wolny czas.

Pytania rekrutacyjne: Concurrent Rendering

Rozmowy techniczne na stanowiska React często badają zrozumienie funkcji concurrent. Te pytania oddzielają kandydatów, którzy wdrożyli produkcyjny kod Suspense, od tych, którzy znają tylko teorię.

P: Co dzieje się, gdy komponent się zawiesza?

Gdy komponent rzuca Promise podczas renderowania, React przechwytuje go na najbliższej granicy Suspense. React renderuje fallbackowe UI i subskrybuje Promise. Gdy się rozwiązuje, React ponownie renderuje zawieszony komponent. Jeśli Promise zostanie odrzucony, błąd propaguje się do najbliższej granicy błędów.

P: Dlaczego render-as-you-fetch ma znaczenie dla Suspense?

Pobieranie podczas renderowania tworzy kaskady zapytań: Parent pobiera, następnie renderuje Child, który pobiera, następnie renderuje Grandchild, który pobiera. Każdy krok czeka na poprzedni. Render-as-you-fetch rozpoczyna wszystkie pobrania przed renderowaniem, więc działają równolegle. Loadery tras i komponenty serwerowe naturalnie wspierają ten wzorzec.

P: Kiedy useTransition może zaszkodzić wydajności?

Transitions utrzymują stare UI widoczne podczas obliczania nowego. Oznacza to, że React renderuje dwukrotnie: raz dla nieaktualnego UI i raz dla świeżego UI. Dla szybkich operacji ten narzut przewyższa korzyści. Używaj transitions tylko dla aktualizacji trwających zauważalnie długo, typowo 100ms lub więcej.

Error Boundaries z Suspense

Suspense obsługuje stany ładowania, ale error boundaries obsługują awarie. Aplikacje produkcyjne potrzebują obu:

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>
  );
}

Error boundary opakowuje granicę Suspense, więc odrzucone Promise uruchamiają UI błędu zamiast powodować crash aplikacji.

Suspense Batching w React 19.2

React 19.2 wprowadził batching Suspense dla renderowania serwerowego, rozwiązując częsty problem: wiele siostrzanych komponentów zawieszających się powodowało wiele round tripów. Przy batchingu React czeka krótko, aby zebrać wiele zawieszeń, a następnie rozwiązuje je razem.

Przed 19.2: Każda granica Suspense streamuje niezależnietsx
// Po 19.2: React batchuje pobliskie zawieszenia

export function Dashboard() {
  return (
    <div className="grid grid-cols-3 gap-4">
      {/* Te teraz są batchowane razem */}
      <Suspense fallback={<CardSkeleton />}>
        <RevenueCard />
      </Suspense>
      <Suspense fallback={<CardSkeleton />}>
        <UsersCard />
      </Suspense>
      <Suspense fallback={<CardSkeleton />}>
        <OrdersCard />
      </Suspense>
    </div>
  );
}

Batching redukuje wizualne migotanie kart pojawiających się jedna po drugiej i poprawia postrzeganą wydajność, prezentując powiązane dane razem.

Wzorce wydajnościowe dla produkcji

Te wzorce adresują typowe problemy produkcyjne z Suspense i concurrent rendering.

Unikaj kaskad Suspense

tsx
// Problem: Sekwencyjne pobieranie
function Parent() {
  const data = use(fetchParent());
  return <Child parentId={data.id} />; // Child pobiera po Parent
}

// Rozwiązanie: Równoległe pobieranie z Promise.all
const [parentPromise, childPromise] = [
  fetchParent(),
  fetchChild()
];

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

Preload przy hover

LinkWithPrefetch.tsxtsx
function LinkWithPrefetch({ href, children }: Props) {
  const router = useRouter();
  
  function handleMouseEnter() {
    // Rozpocznij pobieranie przed kliknięciem
    router.prefetch(href);
  }

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

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Kluczowe wzorce dla funkcji concurrent w React 19

  • Umieszczaj granice Suspense na szwach UI, gdzie niezależne ładowanie ma sens, nie wokół każdego komponentu async
  • Uruchamiaj pobieranie danych w loaderach tras lub komponentach serwerowych, nie wewnątrz funkcji render, aby umożliwić równoległe zapytania
  • Używaj useTransition dla nawigacji i submitów formularzy inicjowanych przez użytkownika, które trwają ponad 100ms
  • Stosuj useDeferredValue dla kosztownych obliczeń pochodnych jak filtrowanie ponad 10 000 elementów
  • Łącz error boundaries z granicami Suspense, aby odrzucone Promise pokazywały UI błędu zamiast crashować
  • Preferuj streaming SSR dla stron z dużą ilością treści, gdzie czas do pierwszego bajtu ma większe znaczenie niż pełna hydracja
  • Testuj funkcje concurrent z React DevTools Profiler, aby identyfikować niepotrzebne re-rendery podczas transitions
Wyzwanie dnia

Znajdziesz błąd w React / Next.js?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 21 sierpnia 2026

Tagi

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

Udostępnij

Powiązane artykuły