Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[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>

Open
#3,764 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Domain
frontend

Research direction

Reproduce with the described @solidjs/vite-plugin SSR setup and inspect hydratedCreateRoot, markTopLevelSnapshotScope, openLiveScope, releaseLiveScope, and armLiveTakeover. Trace why root-scoped takeover gates release before streamed boundaries hydrate, including the keyed store case. Done means the serialized snapshot is adopted when the boundary hydrates, takeover occurs once, and A and B produce no duplicate DOM or errors.

Written by the indexing model from the issue text.

Description

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.

Dominant language
TypeScript
Stars
36.1k
Forks
1.1k
Avg merge
10h 7m
Merged PRs (30d)
289

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from solidjs/solid

All issues in solidjs/solid

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.