Skip to content

CONCEPT Cited by 1 source

Render-as-you-fetch

Render-as-you-fetch is the React-native data-loading pattern that concurrent rendering is designed for. A component renders, its data-fetching hook initiates the fetch, the hook throws a Promise until the data arrives, <Suspense> catches the throw and shows the fallback, and when the Promise resolves React resumes rendering that subtree.

The key property: rendering, not a prior effect, triggers the fetch. Contrast with:

  • Fetch-on-render (React pre-18): useEffect(() => fetch(...)) inside the component — fetch starts after the render commits, forcing a second re-render.
  • Fetch-then-render: fetch in the parent, pass data in as props — decouples fetching from rendering but requires the parent to know what its children need.

Render-as-you-fetch fuses the declarative component tree with the data dependency graph: each component knows what it needs, fetches it as part of rendering, and suspends if not ready.

Canonical shape (theoretical)

// Hypothetical Suspense-compatible data hook.
function ProductDetail({ id }) {
  const product = useProduct(id); // throws a Promise if not ready
  return <div>{product.name}</div>;
}

<Suspense fallback={<Spinner/>}>
  <ProductDetail id="123"/>
</Suspense>

Why Zalando rejected it for Rendering Engine

Zalando's Rendering Engine evaluated this pattern and did a PoC, then turned around. The [[sources/2023-07-10-zalando-rendering-engine-tales-road-to-concurrent-react|2023-07 post]] documents four concrete blockers:

  1. SuspenseList is experimental and limited. RE needs ordered streaming and hydration — Renderers appear in a specific order on the page. The current idiomatic way to constrain Suspense ordering is SuspenseList which is "still to be experimental, with some limitations".

  2. useTransition doesn't consider nested Suspense boundaries. RE composes deeply-nested Renderers (page → collection → product card). The known issue is that useTransition doesn't treat nested Suspense as first-class, producing "bad UX in some scenarios".

  3. Hook-initiated fetches couple fetch timing to render order. "By utilizing hooks to initiate requests or other async operations, the timing of fetch operations becomes coupled with the order of rendering, which may not be optimal for performance." A top-down render walk serialises fetches that could otherwise run in parallel. The framework-side solution would be to resolve data before rendering, write it into a state store, and read from the store in render — which is exactly the inverse of this pattern.

  4. The streaming/caching layer for React data is not final. Progressive hydration and streaming need the data that drove server rendering to be available to the client without a second fetch round-trip. React's supporting feature (facebook/react#25502) was not finalised at PoC time.

Zalando's chosen alternative is to keep React as the renderer but move data resolution out to an Application-State layer outside React that writes data and has a "Connector hook" on the read side. The Connector hook is compatible with Suspense — it returns data or throws a Promise — but the data resolution runs in a plain state machine, not in a render tree.

When it is a good fit

  • Simple per-component data dependencies — one component needs one query; no ordering constraints across siblings.
  • No deep nested Suspense scheduling concernsuseTransition pitfalls don't apply.
  • Data-cache layer available (React Query, SWR, a framework that supplies one) so the client doesn't re-fetch what was server-rendered.
  • Small or simple component tree — the serialisation-through- render-walk cost doesn't dominate.

Next.js App Router + React Server Components is the canonical framework-supplied render-as-you-fetch stack; both conceal the data-cache plumbing from the application and thus side-step the streaming/caching-layer concern.

When to reach for something else

  • Rendering order is business-logic-driven (Zalando RE — personalisation-chosen Entity tree).
  • Fetches across siblings must be parallelised without a prior waterfall.
  • The data-cache-rehydration mechanism is under your control and you want to own it.
  • You have a central state store (Redux-family) that already resolves data for other reasons and pairing it with React's Suspense read path is cheaper than migrating to hooks.

In these cases, patterns/application-state-layer-outside-react tends to be a cleaner architectural fit.

Seen in

Last updated · 501 distilled / 1,218 read