diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 9fb360cf8a5ad..3e4cbb93b4442 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -807,6 +807,7 @@ export type ManualMemoDependency = { } | {kind: 'Global'; identifierName: string}; path: DependencyPath; + loc: SourceLocation; }; export type StartMemoize = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts index 0138e52ef60e4..647562aee591a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts @@ -65,6 +65,7 @@ export function collectMaybeMemoDependencies( identifierName: value.binding.name, }, path: [], + loc: value.loc, }; } case 'PropertyLoad': { @@ -74,6 +75,7 @@ export function collectMaybeMemoDependencies( root: object.root, // TODO: determine if the access is optional path: [...object.path, {property: value.property, optional}], + loc: value.loc, }; } break; @@ -95,6 +97,7 @@ export function collectMaybeMemoDependencies( constant: false, }, path: [], + loc: value.place.loc, }; } break; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts index 5035fa9260f02..2e6217bb51a53 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts @@ -241,11 +241,10 @@ export function validateExhaustiveDependencies( matched.add(manualDependency); } } - const isOptionalDependency = - !reactive.has(inferredDependency.identifier.id) && - (isStableType(inferredDependency.identifier) || - isPrimitiveType(inferredDependency.identifier)); - if (hasMatchingManualDependency || isOptionalDependency) { + if ( + hasMatchingManualDependency || + isOptionalDependency(inferredDependency, reactive) + ) { continue; } missing.push(inferredDependency); @@ -274,54 +273,106 @@ export function validateExhaustiveDependencies( } if (missing.length !== 0 || extra.length !== 0) { - let suggestions: Array | null = null; + let suggestion: CompilerSuggestion | null = null; if (startMemo.depsLoc != null && typeof startMemo.depsLoc !== 'symbol') { - suggestions = [ - { - description: 'Update dependencies', - range: [startMemo.depsLoc.start.index, startMemo.depsLoc.end.index], - op: CompilerSuggestionOperation.Replace, - text: `[${inferred.map(printInferredDependency).join(', ')}]`, - }, - ]; + suggestion = { + description: 'Update dependencies', + range: [startMemo.depsLoc.start.index, startMemo.depsLoc.end.index], + op: CompilerSuggestionOperation.Replace, + text: `[${inferred + .filter( + dep => + dep.kind === 'Local' && !isOptionalDependency(dep, reactive), + ) + .map(printInferredDependency) + .join(', ')}]`, + }; } - if (missing.length !== 0) { - const diagnostic = CompilerDiagnostic.create({ - category: ErrorCategory.MemoDependencies, - reason: 'Found missing memoization dependencies', - description: - 'Missing dependencies can cause a value not to update when those inputs change, ' + - 'resulting in stale UI', - suggestions, + const diagnostic = CompilerDiagnostic.create({ + category: ErrorCategory.MemoDependencies, + reason: 'Found missing/extra memoization dependencies', + description: [ + missing.length !== 0 + ? 'Missing dependencies can cause a value to update less often than it should, ' + + 'resulting in stale UI' + : null, + extra.length !== 0 + ? 'Extra dependencies can cause a value to update more often than it should, ' + + 'resulting in performance problems such as excessive renders or effects firing too often' + : null, + ] + .filter(Boolean) + .join('. '), + suggestions: suggestion != null ? [suggestion] : null, + }); + for (const dep of missing) { + let reactiveStableValueHint = ''; + if (isStableType(dep.identifier)) { + reactiveStableValueHint = + '. Refs, setState functions, and other "stable" values generally do not need to be added ' + + 'as dependencies, but this variable may change over time to point to different values'; + } + diagnostic.withDetails({ + kind: 'error', + message: `Missing dependency \`${printInferredDependency(dep)}\`${reactiveStableValueHint}`, + loc: dep.loc, }); - for (const dep of missing) { - let reactiveStableValueHint = ''; - if (isStableType(dep.identifier)) { - reactiveStableValueHint = - '. Refs, setState functions, and other "stable" values generally do not need to be added as dependencies, but this variable may change over time to point to different values'; - } + } + for (const dep of extra) { + if (dep.root.kind === 'Global') { diagnostic.withDetails({ kind: 'error', - message: `Missing dependency \`${printInferredDependency(dep)}\`${reactiveStableValueHint}`, - loc: dep.loc, + message: + `Unnecessary dependency \`${printManualMemoDependency(dep)}\`. ` + + 'Values declared outside of a component/hook should not be listed as ' + + 'dependencies as the component will not re-render if they change', + loc: dep.loc ?? startMemo.depsLoc ?? value.loc, }); + error.pushDiagnostic(diagnostic); + } else { + const root = dep.root.value; + const matchingInferred = inferred.find( + ( + inferredDep, + ): inferredDep is Extract => { + return ( + inferredDep.kind === 'Local' && + inferredDep.identifier.id === root.identifier.id && + isSubPathIgnoringOptionals(inferredDep.path, dep.path) + ); + }, + ); + if ( + matchingInferred != null && + !isOptionalDependency(matchingInferred, reactive) + ) { + diagnostic.withDetails({ + kind: 'error', + message: + `Overly precise dependency \`${printManualMemoDependency(dep)}\`, ` + + `use \`${printInferredDependency(matchingInferred)}\` instead`, + loc: dep.loc ?? startMemo.depsLoc ?? value.loc, + }); + } else { + /** + * Else this dependency doesn't correspond to anything referenced in the memo function, + * or is an optional dependency so we don't want to suggest adding it + */ + diagnostic.withDetails({ + kind: 'error', + message: `Unnecessary dependency \`${printManualMemoDependency(dep)}\``, + loc: dep.loc ?? startMemo.depsLoc ?? value.loc, + }); + } } - error.pushDiagnostic(diagnostic); - } else if (extra.length !== 0) { - const diagnostic = CompilerDiagnostic.create({ - category: ErrorCategory.MemoDependencies, - reason: 'Found unnecessary memoization dependencies', - description: - 'Unnecessary dependencies can cause a value to update more often than necessary, ' + - 'causing performance regressions and effects to fire more often than expected', - }); + } + if (suggestion != null) { diagnostic.withDetails({ - kind: 'error', - message: `Unnecessary dependencies ${extra.map(dep => `\`${printManualMemoDependency(dep)}\``).join(', ')}`, - loc: startMemo.depsLoc ?? value.loc, + kind: 'hint', + message: `Inferred dependencies: \`${suggestion.text}\``, }); - error.pushDiagnostic(diagnostic); } + error.pushDiagnostic(diagnostic); } dependencies.clear(); @@ -826,3 +877,14 @@ export function findOptionalPlaces( } return optionals; } + +function isOptionalDependency( + inferredDependency: Extract, + reactive: Set, +): boolean { + return ( + !reactive.has(inferredDependency.identifier.id) && + (isStableType(inferredDependency.identifier) || + isPrimitiveType(inferredDependency.identifier)) + ); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts index 48cec3cb12202..9b1bf223332c2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts @@ -242,6 +242,7 @@ function validateInferredDep( normalizedDep = { root: maybeNormalizedRoot.root, path: [...maybeNormalizedRoot.path, ...dep.path], + loc: maybeNormalizedRoot.loc, }; } else { CompilerError.invariant(dep.identifier.name?.kind === 'named', { @@ -270,6 +271,7 @@ function validateInferredDep( constant: false, }, path: [...dep.path], + loc: GeneratedSource, }; } for (const decl of declsWithinMemoBlock) { @@ -383,6 +385,7 @@ class Visitor extends ReactiveFunctionVisitor { constant: false, }, path: [], + loc: storeTarget.loc, }); } } @@ -413,6 +416,7 @@ class Visitor extends ReactiveFunctionVisitor { constant: false, }, path: [], + loc: lvalue.loc, }); } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-exhaustive-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-exhaustive-deps.expect.md deleted file mode 100644 index ed317be118e1e..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-exhaustive-deps.expect.md +++ /dev/null @@ -1,109 +0,0 @@ - -## Input - -```javascript -// @validateExhaustiveMemoizationDependencies -import {useMemo} from 'react'; -import {Stringify} from 'shared-runtime'; - -function Component({x, y, z}) { - const a = useMemo(() => { - return x?.y.z?.a; - // error: too precise - }, [x?.y.z?.a.b]); - const b = useMemo(() => { - return x.y.z?.a; - // ok, not our job to type check nullability - }, [x.y.z.a]); - const c = useMemo(() => { - return x?.y.z.a?.b; - // error: too precise - }, [x?.y.z.a?.b.z]); - const d = useMemo(() => { - return x?.y?.[(console.log(y), z?.b)]; - // ok - }, [x?.y, y, z?.b]); - const e = useMemo(() => { - const e = []; - e.push(x); - return e; - // ok - }, [x]); - const f = useMemo(() => { - return []; - // error: unnecessary - }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]); - const ref1 = useRef(null); - const ref2 = useRef(null); - const ref = z ? ref1 : ref2; - const cb = useMemo(() => { - return () => { - return ref.current; - }; - // error: ref is a stable type but reactive - }, []); - return ; -} - -``` - - -## Error - -``` -Found 4 errors: - -Error: Found missing memoization dependencies - -Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI. - -error.invalid-exhaustive-deps.ts:7:11 - 5 | function Component({x, y, z}) { - 6 | const a = useMemo(() => { -> 7 | return x?.y.z?.a; - | ^^^^^^^^^ Missing dependency `x?.y.z?.a` - 8 | // error: too precise - 9 | }, [x?.y.z?.a.b]); - 10 | const b = useMemo(() => { - -Error: Found missing memoization dependencies - -Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI. - -error.invalid-exhaustive-deps.ts:15:11 - 13 | }, [x.y.z.a]); - 14 | const c = useMemo(() => { -> 15 | return x?.y.z.a?.b; - | ^^^^^^^^^^^ Missing dependency `x?.y.z.a?.b` - 16 | // error: too precise - 17 | }, [x?.y.z.a?.b.z]); - 18 | const d = useMemo(() => { - -Error: Found unnecessary memoization dependencies - -Unnecessary dependencies can cause a value to update more often than necessary, causing performance regressions and effects to fire more often than expected. - -error.invalid-exhaustive-deps.ts:31:5 - 29 | return []; - 30 | // error: unnecessary -> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unnecessary dependencies `x`, `y.z`, `z?.y?.a`, `UNUSED_GLOBAL` - 32 | const ref1 = useRef(null); - 33 | const ref2 = useRef(null); - 34 | const ref = z ? ref1 : ref2; - -Error: Found missing memoization dependencies - -Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI. - -error.invalid-exhaustive-deps.ts:37:13 - 35 | const cb = useMemo(() => { - 36 | return () => { -> 37 | return ref.current; - | ^^^ Missing dependency `ref`. Refs, setState functions, and other "stable" values generally do not need to be added as dependencies, but this variable may change over time to point to different values - 38 | }; - 39 | // error: ref is a stable type but reactive - 40 | }, []); -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-files-with-exhaustive-deps-violation-in-effects.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/compile-files-with-exhaustive-deps-violation-in-effects.expect.md similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-files-with-exhaustive-deps-violation-in-effects.expect.md rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/compile-files-with-exhaustive-deps-violation-in-effects.expect.md diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-files-with-exhaustive-deps-violation-in-effects.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/compile-files-with-exhaustive-deps-violation-in-effects.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/compile-files-with-exhaustive-deps-violation-in-effects.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/compile-files-with-exhaustive-deps-violation-in-effects.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-dep-on-ref-current-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-dep-on-ref-current-value.expect.md new file mode 100644 index 0000000000000..ab9b4080e44b9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-dep-on-ref-current-value.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +// @validateExhaustiveMemoizationDependencies + +function Component() { + const ref = useRef(null); + const onChange = useCallback(() => { + return ref.current.value; + }, [ref.current.value]); + + return ; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Found missing/extra memoization dependencies + +Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often. + +error.invalid-dep-on-ref-current-value.ts:7:6 + 5 | const onChange = useCallback(() => { + 6 | return ref.current.value; +> 7 | }, [ref.current.value]); + | ^^^^^^^^^^^^^^^^^ Unnecessary dependency `ref.current.value` + 8 | + 9 | return ; + 10 | } + +Inferred dependencies: `[]` +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-dep-on-ref-current-value.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-dep-on-ref-current-value.js new file mode 100644 index 0000000000000..f7a3ee43503ae --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-dep-on-ref-current-value.js @@ -0,0 +1,10 @@ +// @validateExhaustiveMemoizationDependencies + +function Component() { + const ref = useRef(null); + const onChange = useCallback(() => { + return ref.current.value; + }, [ref.current.value]); + + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-exhaustive-deps-disallow-unused-stable-types.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps-disallow-unused-stable-types.expect.md similarity index 68% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-exhaustive-deps-disallow-unused-stable-types.expect.md rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps-disallow-unused-stable-types.expect.md index b6d8a46742673..30ab558918dd6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-exhaustive-deps-disallow-unused-stable-types.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps-disallow-unused-stable-types.expect.md @@ -25,18 +25,20 @@ function Component() { ``` Found 1 error: -Error: Found unnecessary memoization dependencies +Error: Found missing/extra memoization dependencies -Unnecessary dependencies can cause a value to update more often than necessary, causing performance regressions and effects to fire more often than expected. +Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often. -error.invalid-exhaustive-deps-disallow-unused-stable-types.ts:11:5 +error.invalid-exhaustive-deps-disallow-unused-stable-types.ts:11:13 9 | return [state]; 10 | // error: `setState` is a stable type, but not actually referenced > 11 | }, [state, setState]); - | ^^^^^^^^^^^^^^^^^ Unnecessary dependencies `setState` + | ^^^^^^^^ Unnecessary dependency `setState` 12 | 13 | return 'oops'; 14 | } + +Inferred dependencies: `[state]` ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-exhaustive-deps-disallow-unused-stable-types.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps-disallow-unused-stable-types.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-exhaustive-deps-disallow-unused-stable-types.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps-disallow-unused-stable-types.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.expect.md new file mode 100644 index 0000000000000..7f94c0390397d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.expect.md @@ -0,0 +1,204 @@ + +## Input + +```javascript +// @validateExhaustiveMemoizationDependencies +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +function Component({x, y, z}) { + const a = useMemo(() => { + return x?.y.z?.a; + // error: too precise + }, [x?.y.z?.a.b]); + const b = useMemo(() => { + return x.y.z?.a; + // ok, not our job to type check nullability + }, [x.y.z.a]); + const c = useMemo(() => { + return x?.y.z.a?.b; + // error: too precise + }, [x?.y.z.a?.b.z]); + const d = useMemo(() => { + return x?.y?.[(console.log(y), z?.b)]; + // ok + }, [x?.y, y, z?.b]); + const e = useMemo(() => { + const e = []; + e.push(x); + return e; + // ok + }, [x]); + const f = useMemo(() => { + return []; + // error: unnecessary + }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]); + const ref1 = useRef(null); + const ref2 = useRef(null); + const ref = z ? ref1 : ref2; + const cb = useMemo(() => { + return () => { + return ref.current; + }; + // error: ref is a stable type but reactive + }, []); + return ; +} + +``` + + +## Error + +``` +Found 5 errors: + +Error: Found missing/extra memoization dependencies + +Missing dependencies can cause a value to update less often than it should, resulting in stale UI. Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often. + +error.invalid-exhaustive-deps.ts:7:11 + 5 | function Component({x, y, z}) { + 6 | const a = useMemo(() => { +> 7 | return x?.y.z?.a; + | ^^^^^^^^^ Missing dependency `x?.y.z?.a` + 8 | // error: too precise + 9 | }, [x?.y.z?.a.b]); + 10 | const b = useMemo(() => { + +error.invalid-exhaustive-deps.ts:9:6 + 7 | return x?.y.z?.a; + 8 | // error: too precise +> 9 | }, [x?.y.z?.a.b]); + | ^^^^^^^^^^^ Overly precise dependency `x?.y.z?.a.b`, use `x?.y.z?.a` instead + 10 | const b = useMemo(() => { + 11 | return x.y.z?.a; + 12 | // ok, not our job to type check nullability + +Inferred dependencies: `[x?.y.z?.a]` + +Error: Found missing/extra memoization dependencies + +Missing dependencies can cause a value to update less often than it should, resulting in stale UI. Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often. + +error.invalid-exhaustive-deps.ts:15:11 + 13 | }, [x.y.z.a]); + 14 | const c = useMemo(() => { +> 15 | return x?.y.z.a?.b; + | ^^^^^^^^^^^ Missing dependency `x?.y.z.a?.b` + 16 | // error: too precise + 17 | }, [x?.y.z.a?.b.z]); + 18 | const d = useMemo(() => { + +error.invalid-exhaustive-deps.ts:17:6 + 15 | return x?.y.z.a?.b; + 16 | // error: too precise +> 17 | }, [x?.y.z.a?.b.z]); + | ^^^^^^^^^^^^^ Overly precise dependency `x?.y.z.a?.b.z`, use `x?.y.z.a?.b` instead + 18 | const d = useMemo(() => { + 19 | return x?.y?.[(console.log(y), z?.b)]; + 20 | // ok + +Inferred dependencies: `[x?.y.z.a?.b]` + +Error: Found missing/extra memoization dependencies + +Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often. + +error.invalid-exhaustive-deps.ts:31:6 + 29 | return []; + 30 | // error: unnecessary +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]); + | ^ Unnecessary dependency `x` + 32 | const ref1 = useRef(null); + 33 | const ref2 = useRef(null); + 34 | const ref = z ? ref1 : ref2; + +error.invalid-exhaustive-deps.ts:31:9 + 29 | return []; + 30 | // error: unnecessary +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]); + | ^^^ Unnecessary dependency `y.z` + 32 | const ref1 = useRef(null); + 33 | const ref2 = useRef(null); + 34 | const ref = z ? ref1 : ref2; + +error.invalid-exhaustive-deps.ts:31:14 + 29 | return []; + 30 | // error: unnecessary +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]); + | ^^^^^^^ Unnecessary dependency `z?.y?.a` + 32 | const ref1 = useRef(null); + 33 | const ref2 = useRef(null); + 34 | const ref = z ? ref1 : ref2; + +error.invalid-exhaustive-deps.ts:31:23 + 29 | return []; + 30 | // error: unnecessary +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]); + | ^^^^^^^^^^^^^ Unnecessary dependency `UNUSED_GLOBAL`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change + 32 | const ref1 = useRef(null); + 33 | const ref2 = useRef(null); + 34 | const ref = z ? ref1 : ref2; + +Inferred dependencies: `[]` + +Error: Found missing/extra memoization dependencies + +Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often. + +error.invalid-exhaustive-deps.ts:31:6 + 29 | return []; + 30 | // error: unnecessary +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]); + | ^ Unnecessary dependency `x` + 32 | const ref1 = useRef(null); + 33 | const ref2 = useRef(null); + 34 | const ref = z ? ref1 : ref2; + +error.invalid-exhaustive-deps.ts:31:9 + 29 | return []; + 30 | // error: unnecessary +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]); + | ^^^ Unnecessary dependency `y.z` + 32 | const ref1 = useRef(null); + 33 | const ref2 = useRef(null); + 34 | const ref = z ? ref1 : ref2; + +error.invalid-exhaustive-deps.ts:31:14 + 29 | return []; + 30 | // error: unnecessary +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]); + | ^^^^^^^ Unnecessary dependency `z?.y?.a` + 32 | const ref1 = useRef(null); + 33 | const ref2 = useRef(null); + 34 | const ref = z ? ref1 : ref2; + +error.invalid-exhaustive-deps.ts:31:23 + 29 | return []; + 30 | // error: unnecessary +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]); + | ^^^^^^^^^^^^^ Unnecessary dependency `UNUSED_GLOBAL`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change + 32 | const ref1 = useRef(null); + 33 | const ref2 = useRef(null); + 34 | const ref = z ? ref1 : ref2; + +Inferred dependencies: `[]` + +Error: Found missing/extra memoization dependencies + +Missing dependencies can cause a value to update less often than it should, resulting in stale UI. + +error.invalid-exhaustive-deps.ts:37:13 + 35 | const cb = useMemo(() => { + 36 | return () => { +> 37 | return ref.current; + | ^^^ Missing dependency `ref`. Refs, setState functions, and other "stable" values generally do not need to be added as dependencies, but this variable may change over time to point to different values + 38 | }; + 39 | // error: ref is a stable type but reactive + 40 | }, []); + +Inferred dependencies: `[ref]` +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-exhaustive-deps.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-exhaustive-deps.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-inner-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-inner-function.expect.md new file mode 100644 index 0000000000000..6d03fb423511d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-inner-function.expect.md @@ -0,0 +1,43 @@ + +## Input + +```javascript +// @validateExhaustiveMemoizationDependencies + +import {useMemo} from 'react'; +import {makeObject_Primitives} from 'shared-runtime'; + +function useHook() { + const object = makeObject_Primitives(); + const fn = useCallback(() => { + const g = () => { + return [object]; + }; + return g; + }); + return fn; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Found missing/extra memoization dependencies + +Missing dependencies can cause a value to update less often than it should, resulting in stale UI. + +error.invalid-missing-nonreactive-dep-inner-function.ts:10:14 + 8 | const fn = useCallback(() => { + 9 | const g = () => { +> 10 | return [object]; + | ^^^^^^ Missing dependency `object` + 11 | }; + 12 | return g; + 13 | }); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-inner-function.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-inner-function.js new file mode 100644 index 0000000000000..02ba7b8406147 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-inner-function.js @@ -0,0 +1,15 @@ +// @validateExhaustiveMemoizationDependencies + +import {useMemo} from 'react'; +import {makeObject_Primitives} from 'shared-runtime'; + +function useHook() { + const object = makeObject_Primitives(); + const fn = useCallback(() => { + const g = () => { + return [object]; + }; + return g; + }); + return fn; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-unmemoized.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-unmemoized.expect.md new file mode 100644 index 0000000000000..a6868ef325c3e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-unmemoized.expect.md @@ -0,0 +1,43 @@ + +## Input + +```javascript +// @validateExhaustiveMemoizationDependencies + +import {useMemo} from 'react'; +import {makeObject_Primitives, useIdentity} from 'shared-runtime'; + +function useHook() { + // object is non-reactive but not memoized bc the mutation surrounds a hook + const object = makeObject_Primitives(); + useIdentity(); + object.x = 0; + const array = useMemo(() => [object], []); + return array; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Found missing/extra memoization dependencies + +Missing dependencies can cause a value to update less often than it should, resulting in stale UI. + +error.invalid-missing-nonreactive-dep-unmemoized.ts:11:31 + 9 | useIdentity(); + 10 | object.x = 0; +> 11 | const array = useMemo(() => [object], []); + | ^^^^^^ Missing dependency `object` + 12 | return array; + 13 | } + 14 | + +Inferred dependencies: `[object]` +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-unmemoized.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-unmemoized.js new file mode 100644 index 0000000000000..c790ce1daee21 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-unmemoized.js @@ -0,0 +1,13 @@ +// @validateExhaustiveMemoizationDependencies + +import {useMemo} from 'react'; +import {makeObject_Primitives, useIdentity} from 'shared-runtime'; + +function useHook() { + // object is non-reactive but not memoized bc the mutation surrounds a hook + const object = makeObject_Primitives(); + useIdentity(); + object.x = 0; + const array = useMemo(() => [object], []); + return array; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep.expect.md new file mode 100644 index 0000000000000..f3423495097da --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +// @validateExhaustiveMemoizationDependencies + +import {useMemo} from 'react'; +import {makeObject_Primitives} from 'shared-runtime'; + +function useHook() { + const object = makeObject_Primitives(); + const array = useMemo(() => [object], []); + return array; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Found missing/extra memoization dependencies + +Missing dependencies can cause a value to update less often than it should, resulting in stale UI. + +error.invalid-missing-nonreactive-dep.ts:8:31 + 6 | function useHook() { + 7 | const object = makeObject_Primitives(); +> 8 | const array = useMemo(() => [object], []); + | ^^^^^^ Missing dependency `object` + 9 | return array; + 10 | } + 11 | + +Inferred dependencies: `[object]` +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep.js new file mode 100644 index 0000000000000..bfc81d9dd5bd3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep.js @@ -0,0 +1,10 @@ +// @validateExhaustiveMemoizationDependencies + +import {useMemo} from 'react'; +import {makeObject_Primitives} from 'shared-runtime'; + +function useHook() { + const object = makeObject_Primitives(); + const array = useMemo(() => [object], []); + return array; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.sketchy-code-exhaustive-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.sketchy-code-exhaustive-deps.expect.md similarity index 78% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.sketchy-code-exhaustive-deps.expect.md rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.sketchy-code-exhaustive-deps.expect.md index 332fee9f06afe..0422a07cd87c5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.sketchy-code-exhaustive-deps.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.sketchy-code-exhaustive-deps.expect.md @@ -25,9 +25,9 @@ function Component() { ``` Found 1 error: -Error: Found missing memoization dependencies +Error: Found missing/extra memoization dependencies -Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI. +Missing dependencies can cause a value to update less often than it should, resulting in stale UI. error.sketchy-code-exhaustive-deps.ts:8:16 6 | const foo = useCallback( @@ -37,6 +37,8 @@ error.sketchy-code-exhaustive-deps.ts:8:16 9 | }, // eslint-disable-next-line react-hooks/exhaustive-deps 10 | [] 11 | ); + +Inferred dependencies: `[item]` ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.sketchy-code-exhaustive-deps.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.sketchy-code-exhaustive-deps.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.sketchy-code-exhaustive-deps.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.sketchy-code-exhaustive-deps.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps-allow-constant-folded-values.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-constant-folded-values.expect.md similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps-allow-constant-folded-values.expect.md rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-constant-folded-values.expect.md diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps-allow-constant-folded-values.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-constant-folded-values.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps-allow-constant-folded-values.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-constant-folded-values.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.expect.md similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.expect.md rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.expect.md diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps.expect.md similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps.expect.md rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps.expect.md diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-unrelated-mutation-in-depslist.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-unrelated-mutation-in-depslist.expect.md index 7d2125259ae8f..7831aeac15d98 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-unrelated-mutation-in-depslist.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-unrelated-mutation-in-depslist.expect.md @@ -32,9 +32,9 @@ function useFoo(input1) { ``` Found 1 error: -Error: Found missing memoization dependencies +Error: Found missing/extra memoization dependencies -Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI. +Missing dependencies can cause a value to update less often than it should, resulting in stale UI. error.useMemo-unrelated-mutation-in-depslist.ts:18:14 16 | const memoized = useMemo(() => { @@ -44,6 +44,8 @@ error.useMemo-unrelated-mutation-in-depslist.ts:18:14 19 | 20 | return [x, memoized]; 21 | } + +Inferred dependencies: `[x, y]` ``` \ No newline at end of file