Skip to content
Twilight React Playground
ثيم رائدaren

DeferredData

componentAdvancedserverbrowserlive demo

Renders a fallback until a promise resolves, then calls a render function with the value; React 19's use() in Suspense.

import { DeferredData, DeferredDataProps } from '@salla.sa/twilight-theme-engine/components/common';

In plain words

Some data can arrive after the rest of the page: reviews, stock counts, a shipping estimate. Instead of making the whole page wait, a loader can hand over a promise (a value that will exist later). DeferredData shows a placeholder until the promise resolves, then calls your function with the result.

If what you pass is already a plain value, it calls your function straight away.

Signature

function DeferredData<T>(props: DeferredDataProps<T>): ReactNode

interface DeferredDataProps<T> {
  promise: Promise<T> | T;           // a plain value renders at once
  fallback: ReactNode;               // shown while the promise is pending
  children: (data: T) => ReactNode;  // a render function, not elements
}

Try it live

DeferredData with a promise that settles after a delay: the fallback shows until then, then the render function gets the value.Try this: once it has loaded, press Re-render: no fallback flash, because the same promise is already known. Set the outcome to reject to see the error reach the error boundary.
Storefront canvas · ar · RTL
Runs in the browser…
Controls
What a theme writes
import { DeferredData, SkeletonPulse } from '@salla.sa/twilight-theme-engine/components/common';

export function ShippingEstimate({ estimate }: { estimate: Promise<string> }) {
  // `estimate` must be the same promise on every render: from a loader, a query or state.
  return (
    <DeferredData promise={estimate} fallback={<SkeletonPulse style={{ width: 220, height: 24 }} />}>
      {(text) => <p>{text}</p>}
    </DeferredData>
  );
}

Example

app/components/product/Reviews.tsx
import { DeferredData } from '@salla.sa/twilight-theme-engine/components/common';
import { SkeletonPulse } from '@salla.sa/twilight-theme-engine/skeleton';

interface Review {
  id: number;
  text: string;
}

// `reviews` comes from a loader: the same promise on every render.
export function Reviews({ reviews }: { reviews: Promise<Review[]> | Review[] }) {
  return (
    <DeferredData promise={reviews} fallback={<SkeletonPulse style={{ height: 120 }} />}>
      {(items) => (
        <ul>
          {items.map((review) => (
            <li key={review.id}>{review.text}</li>
          ))}
        </ul>
      )}
    </DeferredData>
  );
}

How it behaves

  • A promise that is not a Promise goes straight to children, with no <Suspense>, so a loader may return either.

  • It records the promise's state on the promise object itself (under Symbol.for("twilight.deferred")) through .then handlers. Once resolved, rendering again with the same promise calls children synchronously, with no fallback flash.

  • While pending it renders <Suspense fallback={fallback}> around a child that calls React 19's use(promise).

  • A rejected promise is thrown while rendering, so the nearest error boundary (a route's error component) handles it.

  • It shares that state with resolveDeferred. The engine's own pages no longer use it; it is exported for themes.

Gotchas

  • The state lives on the promise object, so a promise created during render (promise={fetchReviews()}) is a new, unknown promise on every render, and the fallback comes back whenever the parent re-renders. Create it once: in a loader, useState, useMemo or a query.

  • Only a real Promise is awaited (instanceof Promise). A thenable from another library is handed to children as if it were the data.

Related

Source and docs