Skip to content

fix: make renderSSR components interactive after resume - #1

Merged
maiieul merged 16 commits into
mainfrom
fix/ssr-resume-interactivity
Jul 27, 2026
Merged

maiieul merged 16 commits into
mainfrom
fix/ssr-resume-interactivity

Conversation

@maiieul

@maiieul maiieul commented Jul 3, 2026 •

Copy link
Copy Markdown
Member

What is it?

  • Bug

The problem

Components rendered with renderSSR() resumed but were never interactive — click handlers silently failed to load. Two stacked causes:

  • local components were SSR-rendered from a temp file whose path-salted segment hashes could never match the client build;
  • Qwik core only attaches vnode refs to a container once per page, so a second renderSSR() on the same page left the container un-resumable.

The first cut also reset that per-page state by hand-deleting core-internal flags (qVNodeData / qVNodeDataProcessed) — fragile and drifting with core (it already missed qVNodeDataStarted). The correct reset is core's own teardown, container.qDestroy(), added in core beta.30. The branch was pinned to beta.24, which predates it, so this PR bumps core off beta.24.

What the core bump revealed

Moving beta.24 → beta.37 surfaced two failures unrelated to the fix:

  • renderHook threw Code(Q3) ("captured variable in the closure can not be serialized"). The check isn't new: beta.24 accidentally resolved core.prod.mjs in dev (qwikVite zeroed Vite 6's development export condition), so dev-only diagnostics never ran under Vitest. beta.28 restored correct dev resolution and beta.29 made the eager QRL-capture check unconditional in dev — so the bump only surfaced a long-intended check that beta.24 had been hiding.
  • the render snapshot changed — beta.37 adds a dev-mode q-d:q-hmr attribute to rendered elements.

Core-side fixes this rides on

Dev SSR here runs in the node vite server, but the serialized QRL segment URLs are fetched by the browser's separate vite server, which never imported the component modules. An earlier revision worked around this by pre-crawling the browser module graph before every render. Two core fixes replace / support that:

  • QwikDev/qwik#8816 — dev load() now bootstraps a QRL segment's parent from the segment URL when it's missing from the requesting server's graph, so the pre-crawl workaround is gone.
  • QwikDev/qwik#8799 — per-container processVNodeData, needed for two live SSR containers on one page.

Both are unreleased, so the core dev dependency points at a pkg.pr.new build that contains both, until they ship in a beta.

What this PR does

  • Reset via qDestroy() — replace the internal-flag deletion with container.qDestroy() on cleanup, scoped to SSR-resumed containers (calling it on CSR containers strips q:container before teardown blur handlers resolve → Code(Q24)).
  • Bump core to a build with #8816 + #8799 (peer >=2.0.0-beta.37).
  • Rework renderHook — build the probe component with componentQrl(inlinedQrl(...)) and pass the hook in as a prop. This avoids component$'s optimizer-only $() (the published dist is never optimizer-transformed) and keeps the hook out of the closure the optimizer serializes — so no Q3, and no noSerialize. componentQrl / getDomContainer are imported, typed, from @qwik.dev/core/internal.
  • Regenerate the render snapshot for the new q-hmr attribute.
  • Restore the two-live-containers test against the #8799 core build.

maiieul added 2 commits July 3, 2026 11:12
renderSSR-rendered components never become interactive: the serialized
QRLs point at segment URLs the client environment cannot serve, so click
handlers silently fail to load. Red on purpose; fix follows.
Serialized dev QRLs point at path-derived segment URLs that qwikVite can
only serve once the segment's parent module is transformed in the client
environment of the vite server actually serving the browser — which is
ctx.project.browser.vite, not ctx.project.vite. Interactions only worked
when an unrelated CSR test happened to import the same component first.

- warm the browser server's client module graph (test file, component
  module, and their local imports) before rendering, instead of building
  a manifest mapping from user segments: a mapping entry makes the
  serializer emit prod-style hash-only symbol names that dev segment
  modules cannot satisfy
- render local components from the test file itself instead of a temp
  copy: segment hashes are path-salted, so temp-file QRLs could never
  resolve against the client-transformed test module; the transform now
  serves registered test files cleaned in non-client environments
- eagerly resume injected containers in renderServerHTML after clearing
  core's processVNodeData once-per-page flag (qVNodeData on older cores,
  qVNodeDataProcessed on newer), so second and later containers on the
  same page get their vnode refs attached

Verified against core 2.0.0-beta.24 (this repo) and 2.0.0-beta.37 +
vite-plus (qwik-lib-template).
@changeset-bot

changeset-bot Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: cd66599

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.

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

@pkg-pr-new

pkg-pr-new Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/vitest-browser-qwik@1

commit: cd66599

maiieul added 2 commits July 3, 2026 12:08
Regression test for the multi-container resume path: a second renderSSR
resume in the same page must not break the first. Passes on core
2.0.0-beta.24 (this repo) and 2.0.0-beta.37 + vite-plus (qwik-lib-template).
@maiieul
maiieul force-pushed the fix/ssr-resume-interactivity branch 2 times, most recently from 89c1843 to 4a8f5fc Compare July 8, 2026 07:42
maiieul added 7 commits July 8, 2026 11:38
- replace the internal vnode-data flag deletion with the container's
  qDestroy() teardown (added in core beta.37)
- rework renderHook as componentQrl(inlinedQrl) with a noSerialize runner
  prop: beta.37's dev build eagerly rejects raw-function QRL captures (Q3)
- update render snapshot for beta.37's dev-mode q-hmr attribute
- drop the two-live-containers test until core #8799 ships in a release
…#8799

Core dev dep points at pkg.pr.new @e70b299 (per-container processVNodeData)
until the fix ships in a released beta.
cleanTestModuleForSSR and its helpers lived in ssr-plugin.ts on main;
keep them there rather than relocating to utils, to keep the diff scoped.
Keep isBrowserOnlySource/referencesStrippedId/isVariableDeclaration where
main had them so the diff shows them unchanged, not moved.
Passing the hook as a prop already keeps it out of the optimizer-serialized
closure; CSR renders never serialize props, so noSerialize was unneeded.
…ernal

They're already typed there, so drop the `as unknown` cast and the now-redundant
getDomContainer guard.
Comment thread src/pure.tsx Outdated
Comment thread src/pure.tsx Outdated
Comment thread src/pure.tsx
Comment thread src/ssr-plugin-utils.ts Outdated
maiieul added 5 commits July 9, 2026 08:26
CSR render() containers have no per-page vnode-data to reset. Calling qDestroy
on them strips q:container before teardown blur handlers resolve, throwing
Code(Q24). Scope qDestroy to renderSSR containers; add a regression test.
Single place to query [q:container] (destroyContainer + resumeQwikContainer),
and explain why renderHook uses componentQrl(inlinedQrl) over component$.
Explain that dev has no manifest (core derives segment URLs from the parent
module), that the manifest only overrides handler symbols, and that warming is
intrinsic — SSR runs in the node server, QRLs resolve in the browser server.
Bump core to the #8816 build (QwikDev/qwik), whose dev load() bootstraps a QRL
segment's parent from the segment URL when it's missing from the requesting
server's graph. That removes the need to pre-crawl the browser module graph
before each SSR render. The build also includes #8799, so the two-container
test still runs.
@maiieul
maiieul force-pushed the fix/ssr-resume-interactivity branch from 172ea56 to cd66599 Compare July 9, 2026 15:13
@maiieul
maiieul merged commit 6ca59be into main Jul 27, 2026
7 checks passed
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.

2 participants