Skip to content

Isolate context id space inside createComponent under NoHydration#2707

Open
LeSingh1 wants to merge 1 commit into
solidjs:mainfrom
LeSingh1:fix-nohydration-resource-id-2546
Open

Isolate context id space inside createComponent under NoHydration#2707
LeSingh1 wants to merge 1 commit into
solidjs:mainfrom
LeSingh1:fix-nohydration-resource-id-2546

Conversation

@LeSingh1
Copy link
Copy Markdown

Closes #2546.

Per the existing maintainer analysis in the issue thread (#2546 / #2131): under <NoHydration>, IDs aren't managed correctly, so two createResource calls in nested components can collide on the same key in context.resources.

Walking through the repro (https://github.com/fongandrew/solidjs-resource-repro):

<NoHydration>
  <Post id="post123">         // outer createComponent
    // const [post] = createResource(...)  -> getNextContextId
    <Suspense>
      <User id={post()?.userId}>   // inner createComponent
        // const [user] = createResource(...) -> getNextContextId

createComponent was:

export function createComponent<T>(Comp: (props: T) => JSX.Element, props: T): JSX.Element {
  if (sharedConfig.context && !sharedConfig.context.noHydrate) {
    const c = sharedConfig.context;
    setHydrateContext(nextHydrateContext());
    const r = Comp(props || ({} as T));
    setHydrateContext(c);
    return r;
  }
  return Comp(props || ({} as T));
}

Under noHydrate the function returned the child directly without pushing a new HydrationContext. So:

  1. Post runs with the parent context. post resource gets id = "parent-0", count becomes 1.
  2. <Suspense> reads the current id ("parent-1") and then runs its body with setHydrateContext({ ...ctx, count: 0 }) — count is reset for the suspense boundary.
  3. User runs with that same parent context (no isolation under noHydrate). user resource gets id = "parent-0" again because count was reset by Suspense — collision with post.resources["parent-0"].

The fix is to always isolate the id space per component, even when noHydrate is set:

export function createComponent<T>(Comp: (props: T) => JSX.Element, props: T): JSX.Element {
  if (sharedConfig.context) {
    const c = sharedConfig.context;
    setHydrateContext(nextHydrateContext());
    const r = Comp(props || ({} as T));
    setHydrateContext(c);
    return r;
  }
  return Comp(props || ({} as T));
}

nextHydrateContext already spreads ...sharedConfig.context, so the new context inherits noHydrate: true and downstream hydration markers continue to be skipped. The only behavior change is that the inner component's resource ids are derived from the outer component's freshly-allocated id, which keeps them unique across Suspense-driven count resets.

Verified: pnpm test in packages/solid → 469 / 469 pass (26 files).

No regression test added because the existing test surface for renderToStringAsync + NoHydration lives in solid-ssr / dom-expressions rather than this package's vitest suite. Happy to add one if you'd like — would need a few lines of harness to wire up dom-expressions/src/server.js here.

When the SSR shared context has noHydrate set, createComponent was
calling the child component directly without pushing a new
HydrationContext. That meant resources created in nested components
shared the parent's id space, and because Suspense resets ctx.count
to 0 inside its boundary, two resources at different positions could
end up keyed by the same context id and the inner resource would
read back the outer one's value.

Always isolate per-component, even under noHydrate. nextHydrateContext
already preserves the noHydrate flag via spread, so downstream hydration
markers are still skipped; the only behavior change is that resource
ids stay unique across components inside <NoHydration>.

Closes solidjs#2546
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 19, 2026

⚠️ No Changeset found

Latest commit: 4acbde5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@ryansolid
Copy link
Copy Markdown
Member

Yeah as I commented in the original issue the challenge here is downstream libraries in the ecosystem like SolidStart literally using the equivalent of Dummy components to match id gen between client/server where the mount and ssr render points are different. I can't make this change without potentially breaking libraries that depend on this behavior. Which is very awkward at this point. At minimum we'd need to makie it a minor release and then be like for this version of Start you need atleast this version of Solid. But more than likely this is just something that gets fixed in Solid 2.0 (and it already is).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

renderToStringAsync with NoHydration and nested resources + Suspense boundaries triggers resource collision

2 participants