Skip to content

feat(router): support ctx.rendered() on streaming (loading()) route trees #548

Closed

Description

@ivogt

Summary

ctx.rendered() (the render barrier) is experimental and Phase 1 only: it is unavailable when the matched route tree uses loading(). Calling it from a streaming tree throws. This issue tracks lifting that limitation so rendered() works under streaming.

Current behavior

rendered() lets a DSL loader await until the non-loader render tree has settled, then read handle data via ctx.use(handle). Today it hard-rejects streaming trees:

  • Guard: src/router/loader-resolution.ts:314-322 throws when reqCtx?._treeHasStreaming is set:
    // Guard: reject streaming trees
    if (reqCtx?._treeHasStreaming) {
      throw new Error(
        `ctx.rendered() is not supported when the matched route tree uses loading(). ` +
        `Streaming handlers may not have settled when rendered() resolves. ...`
      );
    }
  • The flag is computed once per request via treeHasStreaming(ctx.entries) and stashed on the request context (src/router/match-middleware/cache-lookup.ts:197-202 and :582-586; field documented at src/server/request-context.ts:276).
  • The public type is marked **Experimental.** and "non-streaming trees (no loading())" at src/types/loader-types.ts:72.

Why it's blocked

In a streaming tree, handlers behind loading() are React components whose promises resolve during RSC serialization — after resolveAllSegments() returns. So at barrier-resolution time, handle data pushed by those handlers may be incomplete. Resolving the barrier then would hand the loader a partial/empty handle.

Design options (from the rendered-barrier doc)

Supporting streaming requires tracking which handles come from settled vs unsettled handlers, then either:

  1. Wait for full settlement before resolving the barrier — correct, but defeats the point of streaming for that request.
  2. Partial snapshot — resolve the barrier with the settled handles and expose explicit "may be incomplete" semantics for the rest.

Both add real complexity; the Phase 1 decision was to avoid it until there's a concrete use case. Pick/validate an option as part of this work.

Code touchpoints

  • src/router/loader-resolution.ts:305-322 — the rendered() implementation + the streaming guard to lift/replace.
  • src/router/match-middleware/cache-lookup.ts — where the barrier resolves and _treeHasStreaming is set; the resolution point would need to account for unsettled streaming handlers.
  • src/server/request-context.ts:276 — _treeHasStreaming flag.
  • src/types/loader-types.ts:72 — drop/soften **Experimental.** and the "non-streaming" wording once it lands.
  • packages/rangojs-router/docs/internal/rendered-barrier.md — update "Phase 1 scope" / "Why not streaming" and the status line.

Acceptance criteria

  • await ctx.rendered() in a DSL loader on a tree with loading() resolves with correct (or explicitly-scoped) handle data instead of throwing.
  • The chosen semantics (full-wait vs partial-snapshot) are documented and predictable.
  • The handler-invoked-loader guard and the deadlock guard remain (only the streaming guard changes).

Tests

  • src/router/__tests__/rendered-barrier.test.ts:128 (rendered() guard: streaming tree) flips from "throws" to the new behavior.
  • New e2e covering a loading() tree + rendered(), in both dev and production (per the repo's dev+prod mandate).

Notes

This was a documented Phase 1 deferral, not previously tracked work; the doc's own conclusion was "unclear value, avoid for now," so confirm there's a real use case before investing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions