Skip to content

Loading

wybthon.loading

loading

Loading and Reveal: fallback UI for async computations.

Loading is the async boundary (SolidJS 2.0's Loading, the successor to Suspense). It covers initial readiness: any read of an async computation under the boundary that raises NotReadyError registers the computation with the boundary, and the boundary shows its fallback until every registered computation has produced its first value.

Once content is on screen the boundary gets out of the way. A recompute of an async memo that already has a value is a transition: the parts of the UI that depend on the changed input hold their previous state until the new value lands, and the boundary never shows its fallback again. Use is_pending to render inline refresh hints. Pass on= to opt back into the fallback for specific changes (swapping the record being shown rather than refreshing it).

Reveal coordinates multiple Loading boundaries beneath it, controlling the order their contents reveal (order) and whether every pending boundary shows its own fallback (collapsed).

Example
async def load_user():
    return await fetch_json(f"/api/users/{user_id()}")

user = create_memo(load_user)

Loading(
    lambda: div(lambda: user()["name"]),
    fallback=lambda: p("Loading..."),
    on=user_id,
)
See Also

Functions:

Name Description
Loading

Show a fallback while async reads under the boundary aren't ready.

Reveal

Coordinate the reveal order of multiple Loading boundaries.

Loading

Loading(children: Any = None, *, fallback: Any = None, on: Any = None) -> VNode

Show a fallback while async reads under the boundary aren't ready.

Parameters:

Name Type Description Default
children Any

Content rendered when nothing is loading: a VNode, a zero-arg callable, or a list of either. Async computations read anywhere in this subtree self-register with the boundary when a read raises NotReadyError.

None
fallback Any

VNode, string, or callable returning one of those, shown while any registered async computation has no value.

None
on Any

An accessor, or list of accessors, naming the boundary's inputs. The boundary waits for them initially even if the children never read them, and when one of them changes while data under the boundary is pending, the fallback shows again instead of the old content being held. Use it for "new record, fresh boundary" (a route's id), not for refreshes.

None

Returns:

Type Description
VNode

A component VNode that toggles between

VNode

fallback and children.

Example
Loading(
    lambda: Fragment(Header(), Body()),
    fallback=Spinner(),
    on=user_id,
)

Reveal

Reveal(children: Any = None, *, order: RevealOrder = 'sequential', collapsed: bool = False) -> VNode

Coordinate the reveal order of multiple Loading boundaries.

Each Loading boundary mounted underneath (that isn't nested inside another boundary) registers with the Reveal in mount order, and the Reveal decides when each may show its content and whether it shows its fallback. A Reveal nested inside another is one composite slot in the parent's order: its boundaries stay on their fallbacks until the parent releases the slot, then follow the inner order.

Parameters:

Name Type Description Default
children Any

Content containing one or more Loading boundaries.

None
order RevealOrder

"sequential" (default; contents reveal in DOM order, each waiting for the ones before it), "together" (the whole group reveals at once when every boundary has loaded), or "natural" (each boundary reveals on its own data; useful for a nested group that should count as one slot in the parent without coordinating internally).

'sequential'
collapsed bool

Only consulted with order="sequential". When True, boundaries past the current frontier render nothing instead of their own fallback, so only one fallback shows.

False

Returns:

Type Description
VNode

A component VNode.

Example
Reveal(
    [
        Loading(PanelA(), fallback=p("Loading A...")),
        Loading(PanelB(), fallback=p("Loading B...")),
    ],
    collapsed=True,
)
Note

Every boundary's content mounts immediately (parked off-document while pending), so all of them load in parallel; order only controls when each is revealed.

What's in this module

Loading is the async boundary. Any read of an async computation under it that raises NotReadyError registers that computation with the boundary, and the boundary shows its fallback until every registered computation has a first value. Content stays mounted the whole time, parked off-document, so async memos created inside it keep running and nothing is torn down. Revalidations never re-trigger the boundary: a recompute of a memo that has a value is a transition, which holds the affected UI on the old state until the new value lands, and is_pending is the tool for inline refresh hints. on= names inputs whose change should show the fallback again instead (a new record, not a refresh).

Reveal coordinates several Loading boundaries beneath it: the order their contents appear and how many fallbacks show at once.

Name Description
Loading Loading(children, *, fallback=None, on=None); children is a VNode, callable, or list; on is an accessor or list of accessors the boundary waits for, and whose change shows the fallback again.
Reveal Reveal(children, *, order="sequential", collapsed=False); order is "sequential", "together", or "natural"; collapsed=True shows only the next fallback in a sequential group.
from wybthon import Loading, Prop, Reveal, component, create_memo, div, is_pending, p, span

@component
def UserCard(user_id: Prop[int]):
    async def load_user():
        uid = user_id()                    # tracked: refetches when it changes
        return await fetch_json(f"/api/users/{uid}")

    user = create_memo(load_user)
    return Loading(
        lambda: div(
            p(lambda: user()["name"]),
            span(lambda: "Refreshing..." if is_pending(user) else ""),
        ),
        fallback=lambda: p("Loading..."),
    )

@component
def Dashboard(settings: Prop[object]):
    return Reveal(
        [
            Loading(UserCard(user_id=1), fallback=p("Loading user...")),
            Loading(Activity(), fallback=p("Loading activity..."), on=settings),
        ],
        collapsed=True,
    )
  • on= makes the boundary wait for specific accessors even if the children never read them, and when one of them changes while data under the boundary is pending, the fallback shows again instead of the old content being held.
  • With order="sequential" contents reveal in DOM order, each waiting for the ones before it; "together" reveals all at once; "natural" lets each boundary reveal on its own data. With collapsed=True only the next pending fallback shows.
  • Every boundary's content mounts immediately, so all of them load in parallel; order only controls when each is revealed. A Reveal nested inside another is one slot in the parent's order; boundaries nested inside another boundary coordinate with that boundary.

See also