Skip to content

Commit 512bef8

Browse files
committed
fix: preserve innerHTML after component rerender
1 parent ac997ba commit 512bef8

File tree

3 files changed

+73
-2
lines changed

3 files changed

+73
-2
lines changed

.changeset/ninety-crabs-lay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
fix: preserve innerHTML after component rerender

packages/qwik/src/core/shared/shared-serialization.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ import type { StreamWriter } from '../../server/types';
44
import { VNodeDataFlag } from '../../server/types';
55
import type { VNodeData } from '../../server/vnode-data';
66
import { type DomContainer } from '../client/dom-container';
7-
import type { VNode } from '../client/types';
8-
import { vnode_getNode, vnode_isVNode, vnode_locate, vnode_toString } from '../client/vnode';
7+
import type { ElementVNode, VNode } from '../client/types';
8+
import {
9+
ensureMaterialized,
10+
vnode_getNode,
11+
vnode_isVNode,
12+
vnode_locate,
13+
vnode_toString,
14+
} from '../client/vnode';
915
import { isSerializerObj } from '../reactive-primitives/utils';
1016
import type { AsyncComputeQRL, SerializerArg } from '../reactive-primitives/types';
1117
import {
@@ -579,6 +585,32 @@ const allocate = (container: DeserializeContainer, typeId: number, value: unknow
579585
case TypeIds.RefVNode:
580586
const vNode = retrieveVNodeOrDocument(container, value);
581587
if (vnode_isVNode(vNode)) {
588+
/**
589+
* If we have a ref, we need to ensure the element is materialized.
590+
*
591+
* Example:
592+
*
593+
* ```
594+
* const Cmp = component$(() => {
595+
* const element = useSignal<HTMLDivElement>();
596+
*
597+
* useVisibleTask$(() => {
598+
* element.value!.innerHTML = 'I am the innerHTML content!';
599+
* });
600+
*
601+
* return (
602+
* <div ref={element} />
603+
* );
604+
* });
605+
* ```
606+
*
607+
* If we don't materialize early element with ref property, and change element innerHTML it
608+
* will be applied to a vnode tree during the lazy materialization, and it is wrong.
609+
*
610+
* Next if we rerender component it will remove applied innerHTML, because the system thinks
611+
* it is a part of the vnode tree.
612+
*/
613+
ensureMaterialized(vNode as ElementVNode);
582614
return vnode_getNode(vNode);
583615
} else {
584616
throw qError(QError.serializeErrorExpectedVNode, [typeof vNode]);

packages/qwik/src/core/tests/component.spec.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2331,6 +2331,40 @@ describe.each([
23312331
(globalThis as any).logs = undefined;
23322332
});
23332333

2334+
it('should early materialize element with ref property', async () => {
2335+
const Cmp = component$(() => {
2336+
const element = useSignal<HTMLDivElement>();
2337+
const listToForceReRender = useSignal([]);
2338+
2339+
useVisibleTask$(() => {
2340+
element.value!.innerHTML = 'I am the innerHTML content!';
2341+
});
2342+
2343+
return (
2344+
<div>
2345+
<div ref={element} />
2346+
<button
2347+
onClick$={() => {
2348+
listToForceReRender.value = [];
2349+
}}
2350+
>
2351+
Render
2352+
</button>
2353+
{listToForceReRender.value.map(() => (
2354+
<div />
2355+
))}
2356+
</div>
2357+
);
2358+
});
2359+
2360+
const { document } = await render(<Cmp />, { debug });
2361+
if (render === ssrRenderToDom) {
2362+
await trigger(document.body, 'div', 'qvisible');
2363+
}
2364+
await trigger(document.body, 'button', 'click');
2365+
expect(document.body.innerHTML).toContain('I am the innerHTML content!');
2366+
});
2367+
23342368
describe('regression', () => {
23352369
it('#3643', async () => {
23362370
const Issue3643 = component$(() => {

0 commit comments

Comments
 (0)