Skip to content

Fix SSR backpatch serialization for async style and class attributes#8646

Merged
wmertens merged 2 commits into
build/v2from
copilot/fix-useasync-style-resolve
May 19, 2026
Merged

Fix SSR backpatch serialization for async style and class attributes#8646
wmertens merged 2 commits into
build/v2from
copilot/fix-useasync-style-resolve

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 19, 2026

useAsync$ values used in element style were being backpatched with raw object values during SSR, producing [object Object] instead of the serialized attribute string. The same gap affected other special-cased attributes such as class when their resolved value was object-shaped.

  • Backpatch serialization

    • Route async attribute backpatch values through the same serializeAttribute() path used by normal SSR attribute emission.
    • This aligns backpatch behavior with regular attribute generation, including stringification of style objects and normalization of class objects.
  • Regression coverage

    • Add SSR backpatch tests for:
      • useAsync$ resolving into a style object-backed prop
      • useAsync$ resolving into a class object-backed prop
    • Assert that the patched DOM contains serialized attribute strings rather than [object Object].
  • Behavioral impact

    • Async SSR updates now produce the same attribute output shape as initial SSR render for special-cased attributes.
const color = useAsync$(() => Promise.resolve('red'));

return <div style={{ color: color.value }}>I should be red</div>;
// before: style="[object Object]"
// after:  style="color:red"

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 19, 2026

⚠️ No Changeset found

Latest commit: 85ce4ca

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

Copilot AI changed the title [WIP] Fix issue with useAsync$ result not resolving correctly in style Fix SSR backpatch serialization for async style and class attributes May 19, 2026
Copilot AI requested a review from wmertens May 19, 2026 10:02
@wmertens wmertens marked this pull request as ready for review May 19, 2026 16:33
Copilot AI review requested due to automatic review settings May 19, 2026 16:33
@wmertens wmertens requested a review from a team as a code owner May 19, 2026 16:33
@wmertens wmertens enabled auto-merge May 19, 2026 16:33
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 19, 2026

Open in StackBlitz

@qwik.dev/core

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

@qwik.dev/router

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

eslint-plugin-qwik

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

create-qwik

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

@qwik.dev/optimizer

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

commit: 85ce4ca

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aligns SSR backpatching behavior with normal SSR attribute emission by routing promise-resolved attribute values through serializeAttribute(), addressing cases where async-resolved style/class values could otherwise be applied as raw objects (leading to "[object Object]").

Changes:

  • Serialize promise-resolved SSR attribute values via serializeAttribute(key, resolvedValue, styleScopedId) before creating backpatch entries.
  • Add regression tests intended to cover async backpatching for style and class object-shaped attributes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
packages/qwik/src/server/ssr-container.ts Ensures backpatch entries store serialized attribute values (consistent with normal SSR output).
packages/qwik/src/core/tests/backpatch.spec.tsx Adds tests aimed at preventing regressions for async backpatch serialization of style/class.
Comments suppressed due to low confidence (1)

packages/qwik/src/core/tests/backpatch.spec.tsx:309

  • Same issue as the async style test: using isActive.value in render likely triggers component-level Promise retry rather than attribute-level backpatching, so this may not cover the new serializeAttribute() call used when backpatching promise attributes. Consider adjusting the test so the class attribute value is the async value (e.g. AsyncSignal/Promise resolving to a class object), ensuring SSR emits backpatch data and the patched DOM reflects normalized class serialization.
  it('should serialize async class objects before backpatching', async () => {
    const Child = component$<{ isActive: boolean }>(({ isActive }) => {
      return (
        <div
          id="class-target"
          class={{
            active: isActive,
            inactive: !isActive,
          }}
        >
          Styled
        </div>
      );
    });

    const Parent = component$(() => {
      const isActive = useAsync$(() => Promise.resolve(true));
      return <Child isActive={isActive.value} />;
    });

    const { document } = await ssrRenderToDom(<Parent />, { debug });
    const target = document.querySelector('#class-target');

    expect(document.body.innerHTML).toContain(ELEMENT_BACKPATCH_DATA);
    expect(target?.getAttribute('class')).toBe('active');
    expect(target?.outerHTML).not.toContain('[object Object]');
  });

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +261 to +281
it('should serialize async style objects before backpatching', async () => {
const Child = component$<{ color: string }>(({ color }) => {
return (
<div id="style-target" style={{ color }}>
Styled
</div>
);
});

const Parent = component$(() => {
const color = useAsync$(() => Promise.resolve('red'));
return <Child color={color.value} />;
});

const { document } = await ssrRenderToDom(<Parent />, { debug });
const target = document.querySelector('#style-target');

expect(document.body.innerHTML).toContain(ELEMENT_BACKPATCH_DATA);
expect(target?.getAttribute('style')).toBe('color:red');
expect(target?.outerHTML).not.toContain('[object Object]');
});
@wmertens wmertens merged commit 34d6a66 into build/v2 May 19, 2026
35 checks passed
@wmertens wmertens deleted the copilot/fix-useasync-style-resolve branch May 19, 2026 16:52
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.

[🐞] v2: Using result from useAsync$ in an element's style does not resolve correctly

3 participants