Skip to content

fix(core): keep Pending content visible when its owner re-renders after out-of-order streaming - #9167

Closed
intellix wants to merge 2 commits into
QwikDev:mainfrom
intellix:fix/pending-owner-signal-rerender
Closed

intellix wants to merge 2 commits into
QwikDev:mainfrom
intellix:fix/pending-owner-signal-rerender

Conversation

@intellix

@intellix intellix commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #9166

Problem

With out-of-order streaming (the default for <Pending>), the server sets the boundary's state signal to 'fallback' so the shell renders the fallback. That value is serialized into the root state, but on the client qO() only swaps the DOM, so the resumed state stays 'fallback' after the content is shown.

Any later re-render of Pending rebuilds its style signals from that stale state and the fallback reappears. In the issue, the owner's re-render passes a fresh fallback$ QRL, which the diff treats as a changed prop and so re-renders Pending.

Fix

The client moves the boundary's state to 'content' once it sees the swap. The server still serializes 'fallback', which is true for the shell.

  • Swap before resume (the usual browser case, since qO() runs before the container exists): on resume, after segment state is processed, each [q:rp] content host that no longer holds its placeholder template has its Pending state set to 'content'.
  • Swap after resume: qProcessOOOS queues the same step after that segment's state patches.

The state is found through the content host's vnode: its parent is the Pending component, and state is its first hook. Catch's late-fallback q:rp hosts are skipped. Only untrackedValue changes, because qO() has already done the DOM work.

Tests

  • pending.spec.tsx: the issue's scenario, the delayed-fallback variant, and a swap that lands after the container resumed; each fails without its part of the fix
  • pending.e2e.ts: owner re-render fixture across SSR / CSR / pure CSR
  • Also checked the issue's exact route in the router test app (temporary, not committed)

Not in this PR

Components receiving an equivalent $ callback prop re-render whenever their owner re-renders after resume, because the deserialized QRL and the fresh one compare unequal. That's a broader diff change and is left for a separate issue.

🤖 Generated with Claude Code

@intellix
intellix requested a review from a team as a code owner October 5, 2026 16:21
@pkg-pr-new

pkg-pr-new Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@qwik.dev/core

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/core@9167

@qwik.dev/router

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/router@9167

@qwik.dev/utils

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/utils@9167

eslint-plugin-qwik

npm i https://pkg.pr.new/QwikDev/qwik/eslint-plugin-qwik@9167

create-qwik

npm i https://pkg.pr.new/QwikDev/qwik/create-qwik@9167

@qwik.dev/optimizer

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/optimizer@9167

@qwik.dev/devtools

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/devtools@9167

@qwik.dev/mcp

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/mcp@9167

commit: f560272

@changeset-bot

changeset-bot Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f560272

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
@qwik.dev/core Patch
@qwik.dev/devtools Patch
eslint-plugin-qwik Patch
@qwik.dev/mcp Patch
@qwik.dev/react Patch
@qwik.dev/router Patch
@qwik.dev/utils Patch
create-qwik Patch

Not sure what this means? Click here to learn what changesets are.

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

@Varixo

Varixo commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

hey the #9170 fixes part of this PR, so you need to make a new changes on top of them. I will merge my changes asap

@intellix
intellix force-pushed the fix/pending-owner-signal-rerender branch from 5569595 to 4b011dd Compare October 5, 2026 19:18
@intellix

intellix commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on main and dropped my segment vnode-id commit, since #9170 covers it. What's left is just the Pending state fix for #9166.

// The qO() swap reveals the content, so the client must resume in the content state.
function resumeInContentState(state: Signal<PendingState>): void {
state.untrackedValue = 'content';
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks like a workaround, let me think why the state is incorrect

@Varixo

Varixo commented Oct 5, 2026

Copy link
Copy Markdown
Member

So basically the problem is we dont change from fallback to content on client. Your fix is doing a little workaround and set the state 'content' on server, which is incorrect for the flow. It should be changed on the client after swap. I dont have ready solution here, but maybe you can find something

@intellix
intellix force-pushed the fix/pending-owner-signal-rerender branch from 4b011dd to 95ac0f4 Compare October 5, 2026 20:40
@intellix

intellix commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor Author

I just gave it your comment and asked it to investigate and it came up with something else:

Reworked: the server now leaves the state alone and serializes 'fallback' as before. The client moves it to 'content' once it sees the swap: on resume for boundaries qO() already swapped (the usual case, since qO() runs before the container exists), and from qProcessOOOS for swaps that land after resume. The state is found through the content host's vnode (parent is Pending, state is its first hook), and Catch's late-fallback q:rp hosts are skipped. Happy to change how the boundary-to-state link is found if you'd prefer something else.

@Varixo Varixo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new solution looks weird. Imo its still wrong in the flow. It should be somewhere connected to the real content swap, not treat it as done on resume

Comment on lines +119 to +120
// Pending's `state` signal is the first hook it registers.
const PENDING_STATE_SEQ_INDEX = 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is very dangerous to do that, we need to remember about it and update if anything related changes

@intellix

intellix commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

I guess I don't know enough about the core part of qwik so won't be able to deliver this outside of Claude. Hopefully the reproduction and tests are helpful if someone else could look into it

intellix and others added 2 commits October 12, 2026 13:51
…er out-of-order streaming

The server serializes the boundary in its 'fallback' state, and qO() only
swaps the DOM, so the resumed state stayed 'fallback'. Any later Pending
re-render recomputed its styles from it and showed the fallback again.

The client now moves the state to 'content' once the swap is seen: on resume
for boundaries qO() already swapped, and from qProcessOOOS for swaps that land
after resume.

Fixes QwikDev#9166

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tent

Locating a Pending host and reading its render function and hook state while the
container state is still being processed caches those props from unfinished state.
On a larger page that left a component host whose render function was not a QRL, and
every render then threw componentQRL.resolve is not a function in a loop. Mark the
swapped content once the container data is ready, both at resume and for segments
streamed in later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@intellix
intellix force-pushed the fix/pending-owner-signal-rerender branch from 95ac0f4 to f560272 Compare October 12, 2026 13:10
@intellix

Copy link
Copy Markdown
Contributor Author

after your other fix, Claude had to rework it's solution which I've just pushed. It's probably still not pretty but I pushed it anyway as it works for me locally and passes the tests

@Varixo

Varixo commented Oct 25, 2026

Copy link
Copy Markdown
Member

Closing in favour of #9280

@Varixo Varixo closed this Oct 25, 2026
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.

[🐞] <Pending> child reading a signal from its owner breaks when the owner re-renders (consumerBackRef[_EFFECT_BACK_REF].set is not a function)

2 participants