Components (JSX)

Loading

Renders a fallback while pending async reads inside the subtree settle.

Any computation (createMemo, createSignal(fn), createStore(fn), lazy(...), etc.) that throws because data isn't ready is caught by the nearest enclosing <Loading>. The boundary swaps to its fallback until every pending read has resolved, then renders the children.

Once content has rendered, a refetch keeps it visible: the boundary reads the pending value like any other reader and holds the write that made it pending until the data lands (isPending() flips meanwhile).

The optional on prop is a dependency list. The expression is tracked and its value is irrelevant — what matters is what it reads. Whenever anything it reads changes (a plain write, an optimistic write, a source going pending or landing), the boundary stops waiting on its current content and shows fallback again if something under it is still pending, until the new content is ready; if nothing is pending, nothing happens. The fallback lands with the same frame as the change that caused it: immediately when nothing else holds that frame; together with the rest of the new page during a held navigation (a write inside an action, or one whose data other readers are still waiting on) — never a spinner beside a page the change has not reached yet. If the same data is also read outside the boundary, the frame waits on it and the fallback can never be seen; DEV warns LOADING_ON_OUTSIDE_HOLD, and the fix is structural — move the outside read under the boundary so one hold owns the data. A frame held past the content's landing by something else (the write's action, other pending data) shows no fallback either; that is a race, a legitimate outcome, and not reported — show the wait with isPending() instead. A display-ahead read in on (latest()) shows the fallback now, beside the held frame; that is a capability, not the recommended shape. The children are not re-created; they stay alive behind the fallback.

Scope <Loading> around the data-dependent slot, not the surrounding shell. Wrapping layout chrome (header, nav, footer) in the same boundary as the data means revalidation replaces the entire screen with the fallback; rendering chrome outside the boundary keeps it stable while only the affordance flips.


Import

import { Loading } from "solid-js";

Type signature

function Loading(props: {
fallback?: JSX.Element;
on?: any;
children: JSX.Element;
}): JSX.Element;

Props

fallback

  • Type: JSX.Element
  • Optional

Rendered while an async read inside the subtree has no settled value.

on

  • Type: any
  • Optional

A value, not an accessor. When it changes and the new content is not ready, the boundary shows its fallback again instead of holding the update.

children

  • Type: JSX.Element

The subtree whose async reads this boundary handles.


Examples

const Profile = lazy(() => import("./Profile"));
<Loading fallback={<Spinner />}>
<Profile />
</Loading>;
// Depend on the route: a navigation shows the skeleton with the new route
// while the page loads; a refetch of the same route keeps the page visible.
<Loading fallback={<Skeleton />} on={route()}>
<Page />
</Loading>
// Several dependencies: a change to any of them shows the fallback.
<Loading fallback={<Skeleton />} on={[query(), page()]}>
<Results />
</Loading>

Caveats

  • The boundary shows its fallback only when a read inside has no settled value yet. Later updates hold the previous content; use on for content that should show a placeholder again.
  • on compares values, not accessors. Pass on={id()}, not on={id}.
  • Place the boundary around the data-dependent slot, not around layout chrome that should stay stable.

Common problems


Learn more

Last updated: 9/27/26, 8:14 AMEdit this pageReport an issue with this page