Skip to content

Commit e1e45fb

Browse files
authored
[ESLint] Suggest to destructure props when they are only used as members (#14993)
* Suggest to destructure props when they are only used as members * Add more tests * Fix a bug
1 parent 59ef284 commit e1e45fb

File tree

2 files changed

+223
-1
lines changed

2 files changed

+223
-1
lines changed

packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js

+183-1
Original file line numberDiff line numberDiff line change
@@ -2224,7 +2224,189 @@ const tests = {
22242224
}
22252225
`,
22262226
errors: [
2227-
// TODO: make this message clearer since it's not obvious why.
2227+
"React Hook useEffect has a missing dependency: 'props'. " +
2228+
'Either include it or remove the dependency array. ' +
2229+
'Alternatively, destructure the necessary props outside the callback.',
2230+
],
2231+
},
2232+
{
2233+
code: `
2234+
function MyComponent(props) {
2235+
useEffect(() => {
2236+
function play() {
2237+
props.onPlay();
2238+
}
2239+
function pause() {
2240+
props.onPause();
2241+
}
2242+
}, []);
2243+
}
2244+
`,
2245+
output: `
2246+
function MyComponent(props) {
2247+
useEffect(() => {
2248+
function play() {
2249+
props.onPlay();
2250+
}
2251+
function pause() {
2252+
props.onPause();
2253+
}
2254+
}, [props]);
2255+
}
2256+
`,
2257+
errors: [
2258+
"React Hook useEffect has a missing dependency: 'props'. " +
2259+
'Either include it or remove the dependency array. ' +
2260+
'Alternatively, destructure the necessary props outside the callback.',
2261+
],
2262+
},
2263+
{
2264+
code: `
2265+
function MyComponent(props) {
2266+
useEffect(() => {
2267+
if (props.foo.onChange) {
2268+
props.foo.onChange();
2269+
}
2270+
}, []);
2271+
}
2272+
`,
2273+
output: `
2274+
function MyComponent(props) {
2275+
useEffect(() => {
2276+
if (props.foo.onChange) {
2277+
props.foo.onChange();
2278+
}
2279+
}, [props.foo]);
2280+
}
2281+
`,
2282+
errors: [
2283+
"React Hook useEffect has a missing dependency: 'props.foo'. " +
2284+
'Either include it or remove the dependency array.',
2285+
],
2286+
},
2287+
{
2288+
code: `
2289+
function MyComponent(props) {
2290+
useEffect(() => {
2291+
props.onChange();
2292+
if (props.foo.onChange) {
2293+
props.foo.onChange();
2294+
}
2295+
}, []);
2296+
}
2297+
`,
2298+
output: `
2299+
function MyComponent(props) {
2300+
useEffect(() => {
2301+
props.onChange();
2302+
if (props.foo.onChange) {
2303+
props.foo.onChange();
2304+
}
2305+
}, [props]);
2306+
}
2307+
`,
2308+
errors: [
2309+
"React Hook useEffect has a missing dependency: 'props'. " +
2310+
'Either include it or remove the dependency array. ' +
2311+
'Alternatively, destructure the necessary props outside the callback.',
2312+
],
2313+
},
2314+
{
2315+
code: `
2316+
function MyComponent(props) {
2317+
const [skillsCount] = useState();
2318+
useEffect(() => {
2319+
if (skillsCount === 0 && !props.isEditMode) {
2320+
props.toggleEditMode();
2321+
}
2322+
}, [skillsCount, props.isEditMode, props.toggleEditMode]);
2323+
}
2324+
`,
2325+
output: `
2326+
function MyComponent(props) {
2327+
const [skillsCount] = useState();
2328+
useEffect(() => {
2329+
if (skillsCount === 0 && !props.isEditMode) {
2330+
props.toggleEditMode();
2331+
}
2332+
}, [skillsCount, props.isEditMode, props.toggleEditMode, props]);
2333+
}
2334+
`,
2335+
errors: [
2336+
"React Hook useEffect has a missing dependency: 'props'. " +
2337+
'Either include it or remove the dependency array. ' +
2338+
'Alternatively, destructure the necessary props outside the callback.',
2339+
],
2340+
},
2341+
{
2342+
code: `
2343+
function MyComponent(props) {
2344+
const [skillsCount] = useState();
2345+
useEffect(() => {
2346+
if (skillsCount === 0 && !props.isEditMode) {
2347+
props.toggleEditMode();
2348+
}
2349+
}, []);
2350+
}
2351+
`,
2352+
output: `
2353+
function MyComponent(props) {
2354+
const [skillsCount] = useState();
2355+
useEffect(() => {
2356+
if (skillsCount === 0 && !props.isEditMode) {
2357+
props.toggleEditMode();
2358+
}
2359+
}, [props, skillsCount]);
2360+
}
2361+
`,
2362+
errors: [
2363+
"React Hook useEffect has missing dependencies: 'props' and 'skillsCount'. " +
2364+
'Either include them or remove the dependency array. ' +
2365+
'Alternatively, destructure the necessary props outside the callback.',
2366+
],
2367+
},
2368+
{
2369+
code: `
2370+
function MyComponent(props) {
2371+
useEffect(() => {
2372+
externalCall(props);
2373+
props.onChange();
2374+
}, []);
2375+
}
2376+
`,
2377+
output: `
2378+
function MyComponent(props) {
2379+
useEffect(() => {
2380+
externalCall(props);
2381+
props.onChange();
2382+
}, [props]);
2383+
}
2384+
`,
2385+
// Don't suggest to destructure props here since you can't.
2386+
errors: [
2387+
"React Hook useEffect has a missing dependency: 'props'. " +
2388+
'Either include it or remove the dependency array.',
2389+
],
2390+
},
2391+
{
2392+
code: `
2393+
function MyComponent(props) {
2394+
useEffect(() => {
2395+
props.onChange();
2396+
externalCall(props);
2397+
}, []);
2398+
}
2399+
`,
2400+
output: `
2401+
function MyComponent(props) {
2402+
useEffect(() => {
2403+
props.onChange();
2404+
externalCall(props);
2405+
}, [props]);
2406+
}
2407+
`,
2408+
// Don't suggest to destructure props here since you can't.
2409+
errors: [
22282410
"React Hook useEffect has a missing dependency: 'props'. " +
22292411
'Either include it or remove the dependency array.',
22302412
],

packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js

+40
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,46 @@ export default {
449449
}
450450
}
451451

452+
// `props.foo()` marks `props` as a dependency because it has
453+
// a `this` value. This warning can be confusing.
454+
// So if we're going to show it, append a clarification.
455+
if (!extraWarning && missingDependencies.has('props')) {
456+
let propDep = dependencies.get('props');
457+
if (propDep == null) {
458+
return;
459+
}
460+
const refs = propDep.reference.resolved.references;
461+
if (!Array.isArray(refs)) {
462+
return;
463+
}
464+
let isPropsOnlyUsedInMembers = true;
465+
for (let i = 0; i < refs.length; i++) {
466+
const ref = refs[i];
467+
const id = fastFindReferenceWithParent(
468+
componentScope.block,
469+
ref.identifier,
470+
);
471+
if (!id) {
472+
isPropsOnlyUsedInMembers = false;
473+
break;
474+
}
475+
const parent = id.parent;
476+
if (parent == null) {
477+
isPropsOnlyUsedInMembers = false;
478+
break;
479+
}
480+
if (parent.type !== 'MemberExpression') {
481+
isPropsOnlyUsedInMembers = false;
482+
break;
483+
}
484+
}
485+
if (isPropsOnlyUsedInMembers) {
486+
extraWarning =
487+
' Alternatively, destructure the necessary props ' +
488+
'outside the callback.';
489+
}
490+
}
491+
452492
context.report({
453493
node: declaredDependenciesNode,
454494
message:

0 commit comments

Comments
 (0)