Skip to content

Commit d72b4f7

Browse files
wip: extend no-unneccesary-use-callback
1 parent 31d60e5 commit d72b4f7

File tree

2 files changed

+197
-4
lines changed

2 files changed

+197
-4
lines changed

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

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,103 @@ ruleTester.run(RULE_NAME, rule, {
405405
},
406406
},
407407
},
408+
409+
{
410+
code: tsx`
411+
import {useCallback, useState, useEffect} from 'react';
412+
413+
function App({ items }) {
414+
const [test, setTest] = useState(0);
415+
416+
const updateTest = useCallback(() => {setTest(items.length)}, []);
417+
418+
useEffect(() => {
419+
updateTest();
420+
}, [updateTest]);
421+
422+
return <div>items</div>;
423+
}
424+
`,
425+
errors: [
426+
{
427+
messageId: "noUnnecessaryUseCallbackInsideUseEffect",
428+
},
429+
],
430+
settings: {
431+
"react-x": {
432+
importSource: "react",
433+
},
434+
},
435+
},
436+
{
437+
code: tsx`
438+
import {useCallback, useState, useEffect} from 'react';
439+
440+
function App({ items }) {
441+
const [test, setTest] = useState(0);
442+
443+
const updateTest = useCallback(() => {setTest(items.length)}, []);
444+
445+
useEffect(() => {
446+
updateTest();
447+
}, [updateTest]);
448+
449+
return <div>items</div>;
450+
}
451+
452+
function App({ items }) {
453+
const [test, setTest] = useState(0);
454+
455+
const updateTest = useCallback(() => {setTest(items.length)}, []);
456+
457+
useEffect(() => {
458+
updateTest();
459+
}, [updateTest]);
460+
461+
return <div>items</div>;
462+
}
463+
`,
464+
errors: [
465+
{
466+
messageId: "noUnnecessaryUseCallbackInsideUseEffect",
467+
},
468+
{
469+
messageId: "noUnnecessaryUseCallbackInsideUseEffect",
470+
},
471+
],
472+
settings: {
473+
"react-x": {
474+
importSource: "react",
475+
},
476+
},
477+
},
478+
{
479+
code: tsx`
480+
const { useCallback, useEffect } = require("@pika/react");
481+
482+
function App({ items }) {
483+
const [test, setTest] = useState(0);
484+
485+
const updateTest = useCallback(() => {setTest(items.length)}, []);
486+
487+
useEffect(() => {
488+
updateTest();
489+
}, [updateTest]);
490+
491+
return <div>items</div>;
492+
}
493+
`,
494+
errors: [
495+
{
496+
messageId: "noUnnecessaryUseCallbackInsideUseEffect",
497+
},
498+
],
499+
settings: {
500+
"react-x": {
501+
importSource: "@pika/react",
502+
},
503+
},
504+
},
408505
],
409506
valid: [
410507
...allValid,
@@ -501,5 +598,69 @@ ruleTester.run(RULE_NAME, rule, {
501598
const refItem = useCallback(cb, deps)
502599
};
503600
`,
601+
tsx`
602+
import { useCallback, useState, useEffect } from 'react';
603+
604+
function App({ items }) {
605+
const [test, setTest] = useState(items.length);
606+
607+
const updateTest = useCallback(() => { setTest(items.length + 1) }, [setTest, items]);
608+
609+
useEffect(function () {
610+
function foo() {
611+
updateTest();
612+
}
613+
614+
foo();
615+
616+
updateTest();
617+
}, [updateTest])
618+
619+
return <div onClick={() => updateTest()}>{test}</div>;
620+
}
621+
`,
622+
tsx`
623+
import { useCallback, useState, useEffect } from 'react';
624+
625+
const Component = () => {
626+
const [test, setTest] = useState(items.length);
627+
628+
const updateTest = useCallback(() => { setTest(items.length + 1) }, [setTest, items]);
629+
630+
useEffect(() => {
631+
// some condition
632+
doSomeTask();
633+
}, [doSomeTask]);
634+
635+
useEffect(() => {
636+
// some condition
637+
doSomeTask();
638+
}, [doSomeTask]);
639+
640+
return <div />;
641+
};
642+
`,
643+
tsx`
644+
import { useCallback, useState, useEffect } from 'react';
645+
646+
const Component = () => {
647+
const [test, setTest] = useState(items.length);
648+
649+
const updateTest = useCallback(() => { setTest(items.length + 1) }, [setTest, items]);
650+
651+
return <div ref={() => doSomeTask()} />;
652+
};
653+
`,
654+
tsx`
655+
import { useCallback, useState, useEffect } from 'react';
656+
657+
const Component = () => {
658+
const [test, setTest] = useState(items.length);
659+
660+
const updateTest = useCallback(() => { setTest(items.length + 1) }, [setTest, items]);
661+
662+
return <div onClick={doSomeTask} />;
663+
};
664+
`,
504665
],
505666
});

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

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import * as AST from "@eslint-react/ast";
2-
import { isUseCallbackCall } from "@eslint-react/core";
2+
import { isUseCallbackCall, isUseEffectLikeCall } 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";
66
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
7-
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
7+
import { isIdentifier } from "@typescript-eslint/utils/ast-utils";
8+
import { type RuleListener } from "@typescript-eslint/utils/ts-eslint";
89
import type { CamelCase } from "string-ts";
910
import { match } from "ts-pattern";
10-
1111
import { createRule } from "../utils";
1212

1313
export const RULE_NAME = "no-unnecessary-use-callback";
@@ -16,7 +16,7 @@ export const RULE_FEATURES = [
1616
"EXP",
1717
] as const satisfies RuleFeature[];
1818

19-
export type MessageID = CamelCase<typeof RULE_NAME>;
19+
export type MessageID = CamelCase<typeof RULE_NAME> | "noUnnecessaryUseCallbackInsideUseEffect";
2020

2121
export default createRule<[], MessageID>({
2222
meta: {
@@ -28,6 +28,7 @@ export default createRule<[], MessageID>({
2828
messages: {
2929
noUnnecessaryUseCallback:
3030
"An 'useCallback' with empty deps and no references to the component scope may be unnecessary.",
31+
noUnnecessaryUseCallbackInsideUseEffect: "{{name}} is only used inside 1 useEffect may be unnecessary.",
3132
},
3233
schema: [],
3334
},
@@ -39,13 +40,16 @@ export default createRule<[], MessageID>({
3940
export function create(context: RuleContext<MessageID, []>): RuleListener {
4041
// Fast path: skip if `useCallback` is not present in the file
4142
if (!context.sourceCode.text.includes("useCallback")) return {};
43+
4244
return {
4345
CallExpression(node) {
4446
if (!isUseCallbackCall(node)) {
4547
return;
4648
}
49+
4750
const initialScope = context.sourceCode.getScope(node);
4851
const component = context.sourceCode.getScope(node).block;
52+
4953
if (!AST.isFunction(component)) {
5054
return;
5155
}
@@ -99,5 +103,33 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
99103
});
100104
}
101105
},
106+
VariableDeclarator(node) {
107+
if (!context.sourceCode.text.includes("useEffect")) {
108+
return;
109+
}
110+
111+
if (isIdentifier(node.id)) {
112+
const references = context.sourceCode.getDeclaredVariables(node)[0]?.references ?? [];
113+
const usages = references.filter((ref) => !(ref.init ?? false));
114+
115+
const size = usages.reduce((set, usage) => {
116+
const effect = AST.findParentNode(usage.identifier, (node) => {
117+
return isUseEffectLikeCall(node);
118+
});
119+
set.add(effect ?? node.parent);
120+
return set;
121+
}, new Set()).size;
122+
123+
if (size !== 1) {
124+
return;
125+
}
126+
127+
context.report({
128+
messageId: "noUnnecessaryUseCallbackInsideUseEffect",
129+
node,
130+
data: { name: node.id.name },
131+
});
132+
}
133+
},
102134
};
103135
}

0 commit comments

Comments
 (0)