Skip to content

[2.0 rc.10–rc.13] A live source created outside a streamed <Loading> takes over when the shell finishes hydrating, before the boundary's fragment hydrates: unclaimed server DOM + duplicate render, or undefined rows in a keyed <For> #3764

Closed

Description

@ethan-huo

Setup: @solidjs/vite-plugin start mode, ssr: true, production build + vite preview (Node). Reproduces on 2.0.0-rc.10 and 2.0.0-rc.13 (solid-js, @solidjs/web, @solidjs/signals, compiler all at rc.13, vite-plugin next.47), 5/5 runs each. No router, no Cloudflare/workerd needed.

The server answer resolves after 300 ms, so the boundary streams. The browser value is an async iterable branded Symbol.for('solid.LiveSource'); it subscribes inside the first next()'s executor.

function source(liveMs) {
  if (isServer) return (getRequestEvent().locals.answer ??= new Promise(r => setTimeout(() => r(ROWS), 300)))
  let sent = false
  return { [Symbol.for('solid.LiveSource')]: true, [Symbol.asyncIterator]: () => ({
    next: () => sent ? new Promise(() => {}) : new Promise(res => setTimeout(() => (sent = true, res({ done: false, value: ROWS })), liveMs)),
    return: () => Promise.resolve({ done: true }) }) }
}

// A: duplicate render
function A() {
  const rows = createMemo(() => source(600))
  return <Loading fallback={null}><Show when={rows().length > 0}><ul><li>rows: {rows().length}</li></ul></Show></Loading>
}

// B: undefined rows in a keyed <For>
function B() {
  const [rows] = createOptimisticStore(() => source(0), [], { key: 'id' })
  createMemo(() => rows.map(r => r.id).join(','))
  return <Errored fallback={e => <p>{String(e())}</p>}><Loading fallback={null}>
    <ul><For each={[...rows]} keyed={r => r.id}>{r => <li>{r().id}</li>}</For></ul></Loading></Errored>
}

Expected: the source is adopted from the serialized snapshot when its boundary hydrates, then taken over once. One copy of the DOM, no errors.

Actual: the live source subscribes at ~30 ms (when the shell finishes hydrating); the boundary's fragment arrives at ~300 ms.

  • A: the server <ul _hk=…> stays and a second <ul> is client-rendered. In dev: Hydration completed with 1 unclaimed server-rendered node(s).
  • B: TypeError: Cannot read properties of undefined (reading 'id') from the keyed function; the server list stays beside the error fallback.
  • With ssrSource: 'hybrid', A is fixed (takeover waits for the boundary), but B still throws, at takeover time.
  • If the live answer arrives before the fragment and differs from the snapshot, the late snapshot overwrites the newer live value.
  • Dev reproduces too once the server delay exceeds the dev entry's load time (e.g. 4000 ms) — production builds hit it with ordinary delays because the entry loads fast.
  • A source created inside the boundary (its owner is the boundary) behaves correctly: it waits for the boundary to hydrate before subscribing.

Cause (as far as I can read): hydratedCreateRoot → markTopLevelSnapshotScope → openLiveScope(root). Setting sharedConfig.hydrating = false calls releaseLiveScope(root), which releases every root-scoped takeover gate armed by armLiveTakeover while boundaries still waiting on streamed fragments (_pendingBoundaries > 0) read those sources.

Workarounds we use: ssrSource: 'client' for such sources, or create the source inside the streamed boundary; ssrSource: 'hybrid' works for memos but not for stores read by a derived memo.

Happy to share the full minimal Vite app if useful.

Activity

  1. ryansolid commented on Oct 6, 2026

    @ryansolid
    Member

    Fixed by #3817: a live source whose first value is still streaming now takes over at hydration end, so a pending boundary no longer claims against the live replacement. Restoring per-scope takeover for that case is tracked in #3819.

    — Claude via Cursor

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