Skip to content

Commit e963a3e

Browse files
Shinonilesya7
andauthored
New rule 'no-unguarded-host-mutation' (#165)
* new 'no-unguarded-host-mutation' ESLint rule * Make the rule generic * Add 'recommended' to the LWC category * Removing a reference to 'hydration' --------- Co-authored-by: lturanscaia <lturanscaia@salesforce.com>
1 parent 34911de commit e963a3e

File tree

5 files changed

+247
-0
lines changed

5 files changed

+247
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ To choose from three configuration settings, install the [`eslint-config-lwc`](h
8686
| [lwc/no-unsupported-ssr-properties](./docs/rules/no-unsupported-ssr-properties.md) | disallow access of unsupported properties in SSR | |
8787
| [lwc/no-node-env-in-ssr](./docs/rules/no-node-env-in-ssr.md) | disallow usage of process.env.NODE_ENV in SSR | |
8888
| [lwc/valid-graphql-wire-adapter-callback-parameters](./docs/rules/valid-graphql-wire-adapter-callback-parameters.md) | ensure graphql wire adapters are using 'errors' instead of 'error' | |
89+
| [lwc/no-host-mutation-in-connected-callback](./docs/rules/no-host-mutation-in-connected-callback.md) | disallow the host element mutation in 'connectedCallback' | |
8990

9091
### Best practices
9192

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Avoiding host element mutation in connected callback
2+
3+
To ensure consistent behavior in Lightning Web Components (LWC), it is important to avoid mutating the host element within the `connectedCallback` method. Mutations can lead to unpredictable outcomes and inconsistencies in component behavior.
4+
5+
## Rule details
6+
7+
To prevent issues with component lifecycle consistency, avoid mutating the host element in `connectedCallback`. This includes:
8+
9+
- `this.setAttribute`
10+
- `this.classList.add`
11+
12+
Example of **incorrect** code:
13+
14+
```html
15+
<template>
16+
<x-child></x-child>
17+
</template>
18+
```
19+
20+
```js
21+
import { LightningElement, api } from 'lwc';
22+
23+
export default class ExampleComponent extends LightningElement {
24+
@api condition;
25+
26+
connectedCallback() {
27+
this.classList.add(`conditional-class-${this.condition}`);
28+
this.setAttribute('data-some-attribute', 'value');
29+
}
30+
}
31+
```
32+
33+
Example of **correct** code:
34+
35+
```html
36+
<template>
37+
<div class="{theClassMyChildNeeds}">
38+
<x-child></x-child>
39+
</div>
40+
</template>
41+
```
42+
43+
```js
44+
export default class Cmp extends LightningElement {
45+
@api fromOutside;
46+
get theClassMyChildNeeds() {
47+
return `my-child-needs-${this.fromOutside}`;
48+
}
49+
}
50+
```

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const rules = {
3232
'no-restricted-browser-globals-during-ssr': require('./rules/no-restricted-browser-globals-during-ssr'),
3333
'no-unsupported-ssr-properties': require('./rules/no-unsupported-ssr-properties'),
3434
'no-node-env-in-ssr': require('./rules/no-node-env-in-ssr'),
35+
'no-host-mutation-in-connected-callback': require('./rules/no-host-mutation-in-connected-callback'),
3536
};
3637

3738
module.exports = {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright (c) 2024, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
'use strict';
8+
9+
const { docUrl } = require('../util/doc-url');
10+
11+
module.exports = {
12+
meta: {
13+
type: 'problem',
14+
docs: {
15+
description: 'Disallow mutations to the host element in connectedCallback',
16+
category: 'LWC',
17+
recommended: true,
18+
url: docUrl('docs/rules/no-host-mutation-in-connected-callback'),
19+
},
20+
messages: {
21+
noHostMutation: 'Mutations to the host element in connectedCallback are not allowed.',
22+
},
23+
schema: [],
24+
},
25+
create(context) {
26+
return {
27+
MethodDefinition(node) {
28+
if (isConnectedCallback(node)) {
29+
const bodyStatements = node.value.body.body;
30+
31+
for (const statement of bodyStatements) {
32+
if (isMutationStatement(statement)) {
33+
context.report({
34+
node: statement,
35+
messageId: 'noHostMutation',
36+
});
37+
}
38+
}
39+
}
40+
},
41+
};
42+
},
43+
};
44+
45+
function isConnectedCallback(node) {
46+
return node.key.name === 'connectedCallback' && node.value.body.type === 'BlockStatement';
47+
}
48+
49+
function isMutationStatement(statement) {
50+
return (
51+
statement.type === 'ExpressionStatement' &&
52+
statement.expression.type === 'CallExpression' &&
53+
isMutatingHostElement(statement.expression)
54+
);
55+
}
56+
57+
function isMutatingHostElement(expression) {
58+
const callee = expression.callee;
59+
60+
return (
61+
callee.type === 'MemberExpression' &&
62+
(callee.property.name === 'setAttribute' ||
63+
(callee.object &&
64+
callee.object.property &&
65+
callee.object.property.name === 'classList' &&
66+
callee.property.name === 'add'))
67+
);
68+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright (c) 2024, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
'use strict';
8+
9+
const { testRule, testTypeScript } = require('../shared');
10+
11+
testRule('no-host-mutation-in-connected-callback', {
12+
valid: [
13+
{
14+
code: `
15+
import { LightningElement } from 'lwc';
16+
export default class Cmp extends LightningElement {
17+
@api fromOutside;
18+
get theClassMyChildNeeds() {
19+
return \`my-child-needs-\${this.fromOutside}\`;
20+
}
21+
}
22+
`,
23+
},
24+
{
25+
code: `
26+
import { LightningElement } from 'lwc';
27+
export default class Cmp extends LightningElement {
28+
@api fromOutside;
29+
get customAttributes() {
30+
return { class: \`my-child-\${this.fromOutside}\` };
31+
}
32+
}
33+
`,
34+
},
35+
],
36+
invalid: [
37+
{
38+
code: `
39+
import { LightningElement } from 'lwc';
40+
export default class Cmp extends LightningElement {
41+
connectedCallback() {
42+
this.setAttribute('class', \`my-child-\${this.fromOutside}\`);
43+
}
44+
}
45+
`,
46+
errors: [
47+
{
48+
message: 'Mutations to the host element in connectedCallback are not allowed.',
49+
},
50+
],
51+
},
52+
{
53+
code: `
54+
import { LightningElement } from 'lwc';
55+
export default class Cmp extends LightningElement {
56+
connectedCallback() {
57+
this.classList.add(\`my-child-needs-\${this.fromOutside}\`);
58+
}
59+
}
60+
`,
61+
errors: [
62+
{
63+
message: 'Mutations to the host element in connectedCallback are not allowed.',
64+
},
65+
],
66+
},
67+
],
68+
});
69+
70+
testTypeScript('no-host-mutation-in-connected-callback', {
71+
valid: [
72+
{
73+
code: `
74+
import { LightningElement } from 'lwc';
75+
export default class Cmp extends LightningElement {
76+
@api fromOutside: string;
77+
get theClassMyChildNeeds(): string {
78+
return \`my-child-needs-\${this.fromOutside}\`;
79+
}
80+
}
81+
`,
82+
},
83+
{
84+
code: `
85+
import { LightningElement } from 'lwc';
86+
export default class Cmp extends LightningElement {
87+
@api fromOutside: string;
88+
get customAttributes(): { class: string } {
89+
return { class: \`my-child-\${this.fromOutside}\` };
90+
}
91+
}
92+
`,
93+
},
94+
],
95+
invalid: [
96+
{
97+
code: `
98+
import { LightningElement } from 'lwc';
99+
export default class Cmp extends LightningElement {
100+
connectedCallback() {
101+
this.setAttribute('class', \`my-child-\${this.fromOutside}\`);
102+
}
103+
}
104+
`,
105+
errors: [
106+
{
107+
message: 'Mutations to the host element in connectedCallback are not allowed.',
108+
},
109+
],
110+
},
111+
{
112+
code: `
113+
import { LightningElement } from 'lwc';
114+
export default class Cmp extends LightningElement {
115+
connectedCallback() {
116+
this.classList.add(\`my-child-needs-\${this.fromOutside}\`);
117+
}
118+
}
119+
`,
120+
errors: [
121+
{
122+
message: 'Mutations to the host element in connectedCallback are not allowed.',
123+
},
124+
],
125+
},
126+
],
127+
});

0 commit comments

Comments
 (0)