Skip to content

Commit 7d7719f

Browse files
add useCallback rule features to useMemo
1 parent b8437bc commit 7d7719f

File tree

2 files changed

+140
-3
lines changed

2 files changed

+140
-3
lines changed

packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-memo.spec.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,64 @@ ruleTester.run(RULE_NAME, rule, {
9696
},
9797
],
9898
},
99+
100+
{
101+
code: tsx`
102+
import {useMemo, useState, useEffect} from 'react';
103+
104+
function veryHeavyCalculation(items) {
105+
console.log(items)
106+
return items
107+
}
108+
109+
function App({ items }) {
110+
const [test, setTest] = useState(0);
111+
const heavyStuff = useMemo(() => veryHeavyCalculation(items), [items]);
112+
113+
useEffect(() => {
114+
setTest(heavyStuff.length)
115+
}, [heavyStuff]);
116+
117+
return <div>items</div>;
118+
}
119+
`,
120+
errors: [
121+
{
122+
messageId: "noUnnecessaryUseMemoInsideUseEffect",
123+
},
124+
],
125+
settings: {
126+
"react-x": {
127+
importSource: "react",
128+
},
129+
},
130+
},
131+
{
132+
code: tsx`
133+
const { useMemo, useState, useEffect } = require("@pika/react");
134+
135+
function App({ items }) {
136+
const [test, setTest] = useState(0);
137+
const heavyStuff = useMemo(() => veryHeavyCalculation(items), [items]);
138+
139+
useEffect(() => {
140+
setTest(heavyStuff.length)
141+
}, [heavyStuff]);
142+
143+
return <div>items</div>;
144+
}
145+
`,
146+
errors: [
147+
{
148+
messageId: "noUnnecessaryUseMemoInsideUseEffect",
149+
},
150+
],
151+
settings: {
152+
"react-x": {
153+
importSource: "@pika/react",
154+
},
155+
},
156+
},
99157
],
100158
valid: [
101159
...allValid,
@@ -240,5 +298,47 @@ ruleTester.run(RULE_NAME, rule, {
240298
return null;
241299
}
242300
`,
301+
302+
tsx`
303+
import { useMemo, useState, useEffect } from 'react';
304+
305+
function App({ items }) {
306+
const [test, setTest] = useState(0);
307+
const heavyStuff = useMemo(() => veryHeavyCalculation(items), [items]);
308+
309+
useEffect(() => {
310+
setTest(heavyStuff.length)
311+
}, [heavyStuff]);
312+
313+
return <div>{heavyStuff.length}</div>;
314+
}
315+
`,
316+
tsx`
317+
import { useMemo, useState, useEffect } from 'react';
318+
319+
function App({ items }) {
320+
const [test, setTest] = useState(0);
321+
const heavyStuff = useMemo(() => veryHeavyCalculation(items), [items]);
322+
323+
useEffect(() => {
324+
setTest(heavyStuff.length)
325+
}, [heavyStuff]);
326+
327+
useEffect(() => {
328+
console.log(heavyStuff)
329+
}, [heavyStuff]);
330+
331+
return <div>{heavyStuff.length}</div>;
332+
}
333+
`,
334+
tsx`
335+
import { useMemo } from 'react';
336+
337+
function App({ items }) {
338+
const heavyStuff = useMemo(() => veryHeavyCalculation(items), [items]);
339+
340+
return <div>{heavyStuff.length}</div>;
341+
}
342+
`,
243343
],
244344
});

packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-memo.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import * as AST from "@eslint-react/ast";
2-
import { isUseMemoCall } from "@eslint-react/core";
2+
import { isUseEffectLikeCall, isUseMemoCall } from "@eslint-react/core";
33
import { identity } from "@eslint-react/eff";
44
import type { RuleContext, RuleFeature } from "@eslint-react/shared";
55
import { findVariable, getChildScopes, getVariableDefinitionNode } from "@eslint-react/var";
6-
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
6+
import { AST_NODE_TYPES as T, type TSESTree } from "@typescript-eslint/types";
77
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
88
import type { CamelCase } from "string-ts";
99
import { match } from "ts-pattern";
1010

11+
import { isIdentifier } from "@typescript-eslint/utils/ast-utils";
1112
import { createRule } from "../utils";
1213

1314
export const RULE_NAME = "no-unnecessary-use-memo";
@@ -16,7 +17,7 @@ export const RULE_FEATURES = [
1617
"EXP",
1718
] as const satisfies RuleFeature[];
1819

19-
export type MessageID = CamelCase<typeof RULE_NAME>;
20+
export type MessageID = CamelCase<typeof RULE_NAME> | "noUnnecessaryUseMemoInsideUseEffect";
2021

2122
export default createRule<[], MessageID>({
2223
meta: {
@@ -27,6 +28,7 @@ export default createRule<[], MessageID>({
2728
},
2829
messages: {
2930
noUnnecessaryUseMemo: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary.",
31+
noUnnecessaryUseMemoInsideUseEffect: "{{name}} is only used inside 1 useEffect which may be unnecessary.",
3032
},
3133
schema: [],
3234
},
@@ -105,5 +107,40 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
105107
});
106108
}
107109
},
110+
VariableDeclarator(node) {
111+
if (!context.sourceCode.text.includes("useEffect")) {
112+
return;
113+
}
114+
115+
if (!isUseMemoCall(node.init ?? undefined)) {
116+
return;
117+
}
118+
119+
if (!isIdentifier(node.id)) {
120+
return;
121+
}
122+
123+
const references = context.sourceCode.getDeclaredVariables(node)[0]?.references ?? [];
124+
const usages = references.filter((ref) => !(ref.init ?? false));
125+
const effectSet = new Set<TSESTree.Node>();
126+
127+
for (const usage of usages) {
128+
const effect = AST.findParentNode(usage.identifier, (node) => isUseEffectLikeCall(node));
129+
130+
if (effect == null) {
131+
return;
132+
}
133+
134+
effectSet.add(effect);
135+
if (effectSet.size > 1) {
136+
return;
137+
}
138+
}
139+
context.report({
140+
messageId: "noUnnecessaryUseMemoInsideUseEffect",
141+
node,
142+
data: { name: node.id.name },
143+
});
144+
},
108145
};
109146
}

0 commit comments

Comments
 (0)