Skip to content

Commit 4d13710

Browse files
authored
fix(hydration): only ignore mutated host attributes (#4385)
1 parent ae5e7be commit 4d13710

File tree

25 files changed

+188
-15
lines changed

25 files changed

+188
-15
lines changed

packages/@lwc/engine-core/src/framework/hydration.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
APIFeature,
2121
isAPIFeatureEnabled,
2222
isFalse,
23+
StringSplit,
2324
} from '@lwc/shared';
2425

2526
import { logError, logWarn } from '../shared/logger';
@@ -165,9 +166,11 @@ function getValidationPredicate(
165166
optOutStaticProp: string[] | true | undefined
166167
): AttrValidationPredicate {
167168
// `data-lwc-host-mutated` is a special attribute added by the SSR engine itself,
168-
// which does the same thing as an explicit `static validationOptOut = true`.
169-
if (renderer.getAttribute(elm, 'data-lwc-host-mutated') === '') {
170-
return (_attrName: string) => false;
169+
// which does the same thing as an explicit `static validationOptOut = ['attr1', 'attr2']`.
170+
const hostMutatedValue = renderer.getAttribute(elm, 'data-lwc-host-mutated');
171+
if (isString(hostMutatedValue)) {
172+
const mutatedAttrValues = new Set(StringSplit.call(hostMutatedValue, / /));
173+
return (attrName: string) => !mutatedAttrValues.has(attrName);
171174
}
172175

173176
if (isUndefined(optOutStaticProp)) {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<x-cmp aria-label="haha" class="yolo woot" data-foo="bar" data-lwc-host-mutated="aria-label class data-foo">
2+
<template shadowrootmode="open">
3+
</template>
4+
</x-cmp>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const tagName = 'x-cmp';
2+
export { default } from 'x/cmp';
3+
export * from 'x/cmp';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<template>
2+
</template>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { LightningElement } from 'lwc';
2+
3+
export default class extends LightningElement {
4+
connectedCallback() {
5+
// Modify this component's class and attributes at runtime
6+
// We expect a data-lwc-host-mutated attr to be added with the mutated attribute names in unique sorted order
7+
this.setAttribute('data-foo', 'bar')
8+
this.classList.add('yolo')
9+
this.classList.add('woot')
10+
this.setAttribute('aria-label', 'haha')
11+
}
12+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<x-cmp aria-activedescendant="foo" aria-busy="true" data-lwc-host-mutated>
1+
<x-cmp aria-activedescendant="foo" aria-busy="true" data-lwc-host-mutated="aria-activedescendant aria-busy">
22
<template shadowrootmode="open">
33
</template>
44
</x-cmp>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<x-cmp ARIA-LABEL="haha" DATA-FOO="bar" data-lwc-host-mutated="aria-label data-bar data-foo">
2+
<template shadowrootmode="open">
3+
</template>
4+
</x-cmp>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const tagName = 'x-cmp';
2+
export { default } from 'x/cmp';
3+
export * from 'x/cmp';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<template>
2+
</template>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { LightningElement } from 'lwc';
2+
3+
export default class extends LightningElement {
4+
connectedCallback() {
5+
// Modify this component's attributes at runtime, using uppercase
6+
// We expect a data-lwc-host-mutated attr to be added with the mutated attribute names in unique sorted order,
7+
// all lowercase
8+
this.setAttribute('DATA-FOO', 'bar')
9+
this.setAttribute('ARIA-LABEL', 'haha')
10+
this.removeAttribute('dAtA-BaR')
11+
}
12+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<x-getter-class-list class="a c d-e" data-lwc-host-mutated>
1+
<x-getter-class-list class="a c d-e" data-lwc-host-mutated="class">
22
<template shadowrootmode="open">
33
</template>
44
</x-getter-class-list>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<x-method-remove-attribute data-a data-c data-lwc-host-mutated>
1+
<x-method-remove-attribute data-a data-c data-lwc-host-mutated="data-a data-b data-c data-unknown">
22
<template shadowrootmode="open">
33
</template>
44
</x-method-remove-attribute>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<x-method-set-attribute data-boolean="true" data-empty-string data-lwc-host-mutated data-null="null" data-number="1" data-override="override" data-string="test">
1+
<x-method-set-attribute data-boolean="true" data-empty-string data-lwc-host-mutated="data-boolean data-empty-string data-null data-number data-override data-string" data-null="null" data-number="1" data-override="override" data-string="test">
22
<template shadowrootmode="open">
33
</template>
44
</x-method-set-attribute>

packages/@lwc/engine-server/src/renderer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ function getAttribute(element: E, name: string, namespace: string | null = null)
258258
}
259259

260260
function setAttribute(element: E, name: string, value: string, namespace: string | null = null) {
261-
reportMutation(element);
261+
reportMutation(element, name);
262262
const attribute = element[HostAttributesKey].find(
263263
(attr) => attr.name === name && attr[HostNamespaceKey] === namespace
264264
);
@@ -279,7 +279,7 @@ function setAttribute(element: E, name: string, value: string, namespace: string
279279
}
280280

281281
function removeAttribute(element: E, name: string, namespace?: string | null) {
282-
reportMutation(element);
282+
reportMutation(element, name);
283283
element[HostAttributesKey] = element[HostAttributesKey].filter(
284284
(attr) => attr.name !== name && attr[HostNamespaceKey] !== namespace
285285
);
@@ -305,15 +305,15 @@ function getClassList(element: E) {
305305

306306
return {
307307
add(...names: string[]): void {
308-
reportMutation(element);
308+
reportMutation(element, 'class');
309309
const classAttribute = getClassAttribute();
310310

311311
const tokenList = classNameToTokenList(classAttribute.value);
312312
names.forEach((name) => tokenList.add(name));
313313
classAttribute.value = tokenListToClassName(tokenList);
314314
},
315315
remove(...names: string[]): void {
316-
reportMutation(element);
316+
reportMutation(element, 'class');
317317
const classAttribute = getClassAttribute();
318318

319319
const tokenList = classNameToTokenList(classAttribute.value);

packages/@lwc/engine-server/src/utils/mutation-tracking.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,25 @@ const elementsToTrackForMutations: WeakSet<HostElement> = new WeakSet();
1010

1111
const MUTATION_TRACKING_ATTRIBUTE = 'data-lwc-host-mutated';
1212

13-
export function reportMutation(element: HostElement) {
13+
export function reportMutation(element: HostElement, attributeName: string) {
1414
if (elementsToTrackForMutations.has(element)) {
15-
const hasMutationAttribute = element[HostAttributesKey].find(
15+
const existingMutationAttribute = element[HostAttributesKey].find(
1616
(attr) => attr.name === MUTATION_TRACKING_ATTRIBUTE && attr[HostNamespaceKey] === null
1717
);
18-
if (!hasMutationAttribute) {
18+
const attrNameValues = new Set(
19+
existingMutationAttribute ? existingMutationAttribute.value.split(' ') : []
20+
);
21+
attrNameValues.add(attributeName.toLowerCase());
22+
23+
const newMutationAttributeValue = [...attrNameValues].sort().join(' ');
24+
25+
if (existingMutationAttribute) {
26+
existingMutationAttribute.value = newMutationAttributeValue;
27+
} else {
1928
element[HostAttributesKey].push({
2029
name: MUTATION_TRACKING_ATTRIBUTE,
2130
[HostNamespaceKey]: null,
22-
value: '',
31+
value: newMutationAttributeValue,
2332
});
2433
}
2534
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export default {
2+
props: {
3+
ssr: true,
4+
},
5+
clientProps: {
6+
ssr: false,
7+
},
8+
snapshot(target) {
9+
const child = target.shadowRoot.querySelector('x-child');
10+
const div = child.shadowRoot.querySelector('div');
11+
12+
return {
13+
child,
14+
div,
15+
};
16+
},
17+
test(target, snapshots, consoleCalls) {
18+
const snapshotAfterHydration = this.snapshot(target);
19+
expect(snapshotAfterHydration.child).not.toBe(snapshots.child);
20+
expect(snapshotAfterHydration.div).not.toBe(snapshots.div);
21+
22+
const { child } = snapshotAfterHydration;
23+
expect(child.getAttribute('data-foo')).toBe('bar');
24+
expect(child.getAttribute('data-mutatis')).toBe('mutandis');
25+
expect(child.getAttribute('class')).toBe('is-client');
26+
27+
TestUtils.expectConsoleCallsDev(consoleCalls, {
28+
warn: [],
29+
error: [
30+
'Mismatch hydrating element <x-child>: attribute "class" has different values, expected "is-client" but found "is-server"',
31+
'Hydration completed with errors.',
32+
],
33+
});
34+
},
35+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<div>{yolo}</div>
3+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { LightningElement } from 'lwc';
2+
3+
export default class Child extends LightningElement {
4+
yolo = 'woot';
5+
6+
connectedCallback() {
7+
this.setAttribute('data-mutatis', 'mutandis');
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<x-child data-foo="bar" class={mismatchingClass}></x-child>
3+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { LightningElement, api } from 'lwc';
2+
3+
export default class Main extends LightningElement {
4+
@api ssr;
5+
6+
get mismatchingClass() {
7+
return this.ssr ? 'is-server' : 'is-client';
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export default {
2+
props: {
3+
ssr: true,
4+
},
5+
clientProps: {
6+
ssr: false,
7+
},
8+
snapshot(target) {
9+
const child = target.shadowRoot.querySelector('x-child');
10+
const div = child.shadowRoot.querySelector('div');
11+
12+
return {
13+
child,
14+
div,
15+
};
16+
},
17+
test(target, snapshots, consoleCalls) {
18+
const snapshotAfterHydration = this.snapshot(target);
19+
expect(snapshotAfterHydration.child).not.toBe(snapshots.child);
20+
expect(snapshotAfterHydration.div).not.toBe(snapshots.div);
21+
22+
const { child } = snapshotAfterHydration;
23+
expect(child.getAttribute('class')).toBe('static mutatis');
24+
expect(child.getAttribute('data-mismatched-attr')).toBe('is-client');
25+
26+
TestUtils.expectConsoleCallsDev(consoleCalls, {
27+
warn: [],
28+
error: [
29+
'Mismatch hydrating element <x-child>: attribute "data-mismatched-attr" has different values, expected "is-client" but found "is-server"',
30+
'Hydration completed with errors.',
31+
],
32+
});
33+
},
34+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<div>{yolo}</div>
3+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { LightningElement } from 'lwc';
2+
3+
export default class Child extends LightningElement {
4+
yolo = 'woot';
5+
6+
connectedCallback() {
7+
this.classList.add('mutatis');
8+
this.classList.add('mutandis');
9+
this.classList.remove('mutandis');
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<x-child class="static" data-mismatched-attr={mismatchedAttr}></x-child>
3+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { LightningElement, api } from 'lwc';
2+
3+
export default class Main extends LightningElement {
4+
@api ssr;
5+
6+
get mismatchedAttr() {
7+
return this.ssr ? 'is-server' : 'is-client';
8+
}
9+
}

0 commit comments

Comments
 (0)