Skip to content

Commit

Permalink
feat: add ignoredComponents option to require-memo rule
Browse files Browse the repository at this point in the history
  • Loading branch information
Arthur Geron committed Jan 20, 2024
1 parent f8f9441 commit 05f3524
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 12 deletions.
18 changes: 18 additions & 0 deletions __tests__/require-memo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,30 @@ describe('Rule - Require-memo', () => {
{
code: `const Component = memo(() => <div />); export default Component;`,
},
{
code: `export const Component = () => <div />`,
options: [{
ignoredComponents: {
'Component': true,
}
}]
},
],
invalid: [
{
code: `export const Component = () => <div />`,
errors: [{ messageId: "memo-required" }],
},
{
code: `export const ListItem = () => <div />`,
errors: [{ messageId: "memo-required" }],
options: [{
ignoredComponents: {
'*Item': false,
'*': true
}
}]
},
{
code: `const Component = () => <div />; export default Component;`,
errors: [{ messageId: "memo-required" }],
Expand Down
27 changes: 17 additions & 10 deletions docs/rules/require-memo.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
# Rule: require-memo

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.
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.

## Rationale

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.
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.

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

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

export default React.memo(MyComponent);
export default memo(MyComponent);
```

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.

## Rule Details
This rule will enforce that all function components are wrapped in `React.memo()`.
This rule will enforce that all function components are wrapped in `memo()`.
Only exported components are validated.

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

export default React.memo(ComponentB);
export default memo(ComponentB);
```
## Options

The rule takes an optional object:

```json
"rules": {
"@myorg/react-memo/require-memo": [2, {
"ignoreComponents": ["IgnoreMe"]
"@arthurgeron/react-usememo/require-memo": [2, {
"ignoredComponents": {
"IgnoreMe": true,
"DontIgnoreMe": false,
"!IgnoreEverythingButMe": true,
}
}]
}
```
- `ignoreComponents`: An array of component names to ignore when checking for `React.memo()` usage.
- `{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.
You can use strict 1:1 comparisons (e.g., `"ComponentName"`) or employ Minimatch's Glob Pattern (e.g., `"*Item"`).
> 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.

## When Not To Use It

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.
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.

> For more examples and detailed explanation, refer to the eslint-plugin-react-memo [readme](https://github.com/myorg/eslint-plugin-react-memo).
7 changes: 7 additions & 0 deletions src/require-memo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ const rule: Rule.RuleModule = {
messages: {
"memo-required": "Component definition not wrapped in React.memo()",
},
schema: [
{
type: "object",
properties: { ignoredComponents: {type: "object"} },
additionalProperties: false,
},
],
},
create: (context) => ({
ExportNamedDeclaration(node) {
Expand Down
9 changes: 7 additions & 2 deletions src/require-memo/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@

import type { Rule } from "eslint";
import type * as ESTree from "estree";
import { isComponentName } from '../utils';
import { isComponentName, shouldIgnoreNode } from '../utils';
import * as path from "path";

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

function isMemoCallExpression(node: Rule.Node) {
if (node.type !== "CallExpression") return false;
Expand Down Expand Up @@ -37,6 +38,7 @@ export function checkFunction(
) &
Rule.NodeParentExtension
) {
const ignoredNames = context.options?.[0]?.ignoredComponents;
let currentNode = node.type === 'FunctionDeclaration' ? node : node.parent;
while (currentNode.type === "CallExpression") {
if (isMemoCallExpression(currentNode)) {
Expand All @@ -49,14 +51,17 @@ export function checkFunction(
if (currentNode.type === "VariableDeclarator" || currentNode.type === 'FunctionDeclaration') {
const { id } = currentNode;
if (id?.type === "Identifier") {
if (isComponentName(id?.name)) {
if (isComponentName(id?.name) && (!ignoredNames || !shouldIgnoreNode(id as unknown as ESNode, ignoredNames))) {
context.report({ node, messageId: "memo-required" });
}
}
} else if (
node.type === "FunctionDeclaration" &&
currentNode.type === "Program"
) {
if(ignoredNames && !shouldIgnoreNode(node as unknown as ESNode, ignoredNames)) {
return;
}
if (node.id !== null &&isComponentName(node.id?.name)) {
context.report({ node, messageId: "memo-required" });
} else {
Expand Down

0 comments on commit 05f3524

Please sign in to comment.