DeferredData
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
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
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
promisethat is not aPromisegoes straight tochildren, 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.thenhandlers. Once resolved, rendering again with the same promise callschildrensynchronously, with no fallback flash.While pending it renders
<Suspense fallback={fallback}>around a child that calls React 19'suse(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,useMemoor a query.Only a real
Promiseis awaited (instanceof Promise). A thenable from another library is handed tochildrenas if it were the data.
Related
Source and docs
- Engine source:
packages/theme-engine/src/components/common/DeferredData.tsx