Skip to content

Commit 05f3524

Browse files
author
Arthur Geron
committed
feat: add ignoredComponents option to require-memo rule
1 parent f8f9441 commit 05f3524

File tree

4 files changed

+49
-12
lines changed

4 files changed

+49
-12
lines changed

__tests__/require-memo.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,30 @@ describe('Rule - Require-memo', () => {
8585
{
8686
code: `const Component = memo(() => <div />); export default Component;`,
8787
},
88+
{
89+
code: `export const Component = () => <div />`,
90+
options: [{
91+
ignoredComponents: {
92+
'Component': true,
93+
}
94+
}]
95+
},
8896
],
8997
invalid: [
9098
{
9199
code: `export const Component = () => <div />`,
92100
errors: [{ messageId: "memo-required" }],
93101
},
102+
{
103+
code: `export const ListItem = () => <div />`,
104+
errors: [{ messageId: "memo-required" }],
105+
options: [{
106+
ignoredComponents: {
107+
'*Item': false,
108+
'*': true
109+
}
110+
}]
111+
},
94112
{
95113
code: `const Component = () => <div />; export default Component;`,
96114
errors: [{ messageId: "memo-required" }],

docs/rules/require-memo.md

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
# Rule: require-memo
22

3-
This rule enforces the use of `React.memo()` on function components. The objective is to optimize your component re-renders and avoid unnecessary render cycles when your component's props do not change.
3+
This rule enforces the use of `memo()` on function components. The objective is to optimize your component re-renders and avoid unnecessary render cycles when your component's props do not change.
44

55
## Rationale
66

7-
React’s rendering behavior ensures that whenever the parent component renders, the child component instances are re-rendered as well. When dealing with expensive computations or components, this could lead to performance issues. `React.memo()` is a higher order component which tells React to skip rendering the component if its props have not changed.
7+
React’s rendering behavior ensures that whenever the parent component renders, the child component instances are re-rendered as well. When dealing with expensive computations or components, this could lead to performance issues. `memo()` is a higher order component which tells React to skip rendering the component if its props have not changed.
88

9-
When `React.memo()` wraps an exported component, then it will only re-render if the current and next props are not shallowly equal.
9+
When `memo()` wraps an exported component, then it will only re-render if the current and next props are not shallowly equal.
1010

1111
```jsx
1212
function MyComponent(props) { /* ... */ }
1313

14-
export default React.memo(MyComponent);
14+
export default memo(MyComponent);
1515
```
1616

1717
This rule applies to function components, not class-based components as they should extend `React.PureComponent` or must implement `shouldComponentUpdate` lifecycle method for similar optimization.
1818

1919
## Rule Details
20-
This rule will enforce that all function components are wrapped in `React.memo()`.
20+
This rule will enforce that all function components are wrapped in `memo()`.
2121
Only exported components are validated.
2222

2323
## Incorrect Code Examples
@@ -43,23 +43,30 @@ function ComponentB(props) {
4343
return <div>{props.name}</div>;
4444
}
4545

46-
export default React.memo(ComponentB);
46+
export default memo(ComponentB);
4747
```
4848
## Options
4949

5050
The rule takes an optional object:
5151

5252
```json
5353
"rules": {
54-
"@myorg/react-memo/require-memo": [2, {
55-
"ignoreComponents": ["IgnoreMe"]
54+
"@arthurgeron/react-usememo/require-memo": [2, {
55+
"ignoredComponents": {
56+
"IgnoreMe": true,
57+
"DontIgnoreMe": false,
58+
"!IgnoreEverythingButMe": true,
59+
}
5660
}]
5761
}
5862
```
59-
- `ignoreComponents`: An array of component names to ignore when checking for `React.memo()` usage.
63+
- `{ignoredComponents: Record<string, boolean>}`: This allows you to add specific Component Names, thereby individually disabling or enabling them to be checked when used. Matching names with a `true` value will cause the checks to be ignored.
64+
You can use strict 1:1 comparisons (e.g., `"ComponentName"`) or employ Minimatch's Glob Pattern (e.g., `"*Item"`).
65+
> For more information on Minimatch, refer to its README [here](https://www.npmjs.com/package/minimatch). You may also find this [cheatsheet](https://github.com/motemen/minimatch-cheat-sheet) useful.
66+
6067

6168
## When Not To Use It
6269

63-
If the component always re-renders with different props or is not expensive in terms of performance, there is no real benefit to using `React.memo()`. In fact, using `React.memo()` for a large number of simple components could negatively impact performance as memoizing small components may cost more than re-rendering them. So this rule should be disabled in such scenarios.
70+
If the component always re-renders with different props or is not expensive in terms of performance, there is no real benefit to using `memo()`. In fact, using `memo()` for a large number of simple components could negatively impact performance as memoizing small components may cost more than re-rendering them.
6471

6572
> For more examples and detailed explanation, refer to the eslint-plugin-react-memo [readme](https://github.com/myorg/eslint-plugin-react-memo).

src/require-memo/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ const rule: Rule.RuleModule = {
99
messages: {
1010
"memo-required": "Component definition not wrapped in React.memo()",
1111
},
12+
schema: [
13+
{
14+
type: "object",
15+
properties: { ignoredComponents: {type: "object"} },
16+
additionalProperties: false,
17+
},
18+
],
1219
},
1320
create: (context) => ({
1421
ExportNamedDeclaration(node) {

src/require-memo/utils.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11

22
import type { Rule } from "eslint";
33
import type * as ESTree from "estree";
4-
import { isComponentName } from '../utils';
4+
import { isComponentName, shouldIgnoreNode } from '../utils';
55
import * as path from "path";
66

77
import type { MemoFunctionExpression, MemoVariableIdentifier} from './types';
8+
import { ESNode } from "src/types";
89

910
function isMemoCallExpression(node: Rule.Node) {
1011
if (node.type !== "CallExpression") return false;
@@ -37,6 +38,7 @@ export function checkFunction(
3738
) &
3839
Rule.NodeParentExtension
3940
) {
41+
const ignoredNames = context.options?.[0]?.ignoredComponents;
4042
let currentNode = node.type === 'FunctionDeclaration' ? node : node.parent;
4143
while (currentNode.type === "CallExpression") {
4244
if (isMemoCallExpression(currentNode)) {
@@ -49,14 +51,17 @@ export function checkFunction(
4951
if (currentNode.type === "VariableDeclarator" || currentNode.type === 'FunctionDeclaration') {
5052
const { id } = currentNode;
5153
if (id?.type === "Identifier") {
52-
if (isComponentName(id?.name)) {
54+
if (isComponentName(id?.name) && (!ignoredNames || !shouldIgnoreNode(id as unknown as ESNode, ignoredNames))) {
5355
context.report({ node, messageId: "memo-required" });
5456
}
5557
}
5658
} else if (
5759
node.type === "FunctionDeclaration" &&
5860
currentNode.type === "Program"
5961
) {
62+
if(ignoredNames && !shouldIgnoreNode(node as unknown as ESNode, ignoredNames)) {
63+
return;
64+
}
6065
if (node.id !== null &&isComponentName(node.id?.name)) {
6166
context.report({ node, messageId: "memo-required" });
6267
} else {

0 commit comments

Comments
 (0)