Fix SSR backpatch serialization for async style and class attributes#8646
Conversation
|
style and class attributes
@qwik.dev/core
@qwik.dev/router
eslint-plugin-qwik
create-qwik
@qwik.dev/optimizer
commit: |
There was a problem hiding this comment.
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
styleandclassobject-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
styletest: usingisActive.valuein render likely triggers component-level Promise retry rather than attribute-level backpatching, so this may not cover the newserializeAttribute()call used when backpatching promise attributes. Consider adjusting the test so theclassattribute 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.
| 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]'); | ||
| }); |
useAsync$values used in elementstylewere 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 asclasswhen their resolved value was object-shaped.Backpatch serialization
serializeAttribute()path used by normal SSR attribute emission.styleobjects and normalization ofclassobjects.Regression coverage
useAsync$resolving into astyleobject-backed propuseAsync$resolving into aclassobject-backed prop[object Object].Behavioral impact