@samitouri / QOS-React / commits / ec9cc003d2

[compiler][poc] Reuse ValidateExhaustiveDeps for effect dep validation (#35285)

Alternative approach to #35282 for validating effect deps in the compiler that builds on the machinery in ValidateExhaustiveDependencies. Key changes to that pass: * Refactor to track the dependencies of array expressions as temporaries so we can look them up later if they appear as effect deps. * Instead of not storing temporaries for LoadLocals of locally created variables, we store the temporary but also propagate the local-ness through. This allows us to record deps at the top level, necessary for effect deps. Previously the pass was only ever concerned with tracking deps within function expressions. * Refactor the bulk of the dependency-checking logic from `onFinishMemoize()` into a standalone helper to use it for the new `onEffect()` helper as well. * Add a new ErrorCategory for effect deps, use it for errors on effects * Put the effect dep validation behind a feature flag * Adjust the error reason for effect errors --------- Co-authored-by: Jack Pope <jackpope1@gmail.com>

Joseph Savona committed Dec 8, 2025 at 07:58 UTC ec9cc003d232b5a3eed735311df152463a883b32
20 files changed +924 -303
compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts
+16 -1
@@ -601,7 +601,8 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
601 case ErrorCategory.Syntax:
602 case ErrorCategory.UseMemo:
603 case ErrorCategory.VoidUseMemo:
604 - case ErrorCategory.MemoDependencies: {
604 + case ErrorCategory.MemoDependencies:
605 + case ErrorCategory.EffectExhaustiveDependencies: {
606 heading = 'Error';
607 break;
608 }
@@ -683,6 +684,10 @@ export enum ErrorCategory {
684 * Checks for memoized effect deps
685 */
686 EffectDependencies = 'EffectDependencies',
687 + /**
688 + * Checks for exhaustive and extraneous effect dependencies
689 + */
690 + EffectExhaustiveDependencies = 'EffectExhaustiveDependencies',
691 /**
692 * Checks for no setState in effect bodies
693 */
@@ -838,6 +843,16 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
843 preset: LintRulePreset.Off,
844 };
845 }
846 + case ErrorCategory.EffectExhaustiveDependencies: {
847 + return {
848 + category,
849 + severity: ErrorSeverity.Error,
850 + name: 'exhaustive-effect-dependencies',
851 + description:
852 + 'Validates that effect dependencies are exhaustive and without extraneous values',
853 + preset: LintRulePreset.Off,
854 + };
855 + }
856 case ErrorCategory.EffectDerivationsOfState: {
857 return {
858 category,
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+4 -1
@@ -304,7 +304,10 @@ function runWithEnvironment(
304 log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
305
306 if (env.enableValidations) {
307 - if (env.config.validateExhaustiveMemoizationDependencies) {
307 + if (
308 + env.config.validateExhaustiveMemoizationDependencies ||
309 + env.config.validateExhaustiveEffectDependencies
310 + ) {
311 // NOTE: this relies on reactivity inference running first
312 validateExhaustiveDependencies(hir).unwrap();
313 }
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+5
@@ -223,6 +223,11 @@ export const EnvironmentConfigSchema = z.object({
223 */
224 validateExhaustiveMemoizationDependencies: z.boolean().default(true),
225
226 + /**
227 + * Validate that dependencies supplied to effect hooks are exhaustive.
228 + */
229 + validateExhaustiveEffectDependencies: z.boolean().default(false),
230 +
231 /**
232 * When this is true, rather than pruning existing manual memoization but ensuring or validating
233 * that the memoized values remain memoized, the compiler will simply not prune existing calls to
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts
+437 -243
@@ -10,6 +10,7 @@ import {
10 CompilerDiagnostic,
11 CompilerError,
12 CompilerSuggestionOperation,
13 + Effect,
14 SourceLocation,
15 } from '..';
16 import {CompilerSuggestion, ErrorCategory} from '../CompilerError';
@@ -18,10 +19,12 @@ import {
19 BlockId,
20 DependencyPath,
21 FinishMemoize,
22 + GeneratedSource,
23 HIRFunction,
24 Identifier,
25 IdentifierId,
26 InstructionKind,
27 + isEffectEventFunctionType,
28 isPrimitiveType,
29 isStableType,
30 isSubPath,
@@ -40,6 +43,7 @@ import {
43 } from '../HIR/visitors';
44 import {Result} from '../Utils/Result';
45 import {retainWhere} from '../Utils/utils';
46 +import {isEffectHook} from './ValidateMemoizedEffectDependencies';
47
48 const DEBUG = false;
49
@@ -85,6 +89,7 @@ const DEBUG = false;
89 export function validateExhaustiveDependencies(
90 fn: HIRFunction,
91 ): Result<void, CompilerError> {
92 + const env = fn.env;
93 const reactive = collectReactiveIdentifiersHIR(fn);
94
95 const temporaries: Map<IdentifierId, Temporary> = new Map();
@@ -126,270 +131,344 @@ export function validateExhaustiveDependencies(
131 loc: value.loc,
132 },
133 );
129 - visitCandidateDependency(value.decl, temporaries, dependencies, locals);
130 - const inferred: Array<InferredDependency> = Array.from(dependencies);
131 - // Sort dependencies by name and path, with shorter/non-optional paths first
132 - inferred.sort((a, b) => {
133 - if (a.kind === 'Global' && b.kind == 'Global') {
134 - return a.binding.name.localeCompare(b.binding.name);
135 - } else if (a.kind == 'Local' && b.kind == 'Local') {
136 - CompilerError.simpleInvariant(
137 - a.identifier.name != null &&
138 - a.identifier.name.kind === 'named' &&
139 - b.identifier.name != null &&
140 - b.identifier.name.kind === 'named',
141 - {
142 - reason: 'Expected dependencies to be named variables',
143 - loc: a.loc,
144 - },
145 - );
146 - if (a.identifier.id !== b.identifier.id) {
147 - return a.identifier.name.value.localeCompare(b.identifier.name.value);
134 + if (env.config.validateExhaustiveMemoizationDependencies) {
135 + visitCandidateDependency(value.decl, temporaries, dependencies, locals);
136 + const inferred: Array<InferredDependency> = Array.from(dependencies);
137 +
138 + const diagnostic = validateDependencies(
139 + inferred,
140 + startMemo.deps ?? [],
141 + reactive,
142 + startMemo.depsLoc,
143 + ErrorCategory.MemoDependencies,
144 + );
145 + if (diagnostic != null) {
146 + error.pushDiagnostic(diagnostic);
147 + }
148 + }
149 +
150 + dependencies.clear();
151 + locals.clear();
152 + startMemo = null;
153 + }
154 +
155 + collectDependencies(
156 + fn,
157 + temporaries,
158 + {
159 + onStartMemoize,
160 + onFinishMemoize,
161 + onEffect: (inferred, manual, manualMemoLoc) => {
162 + if (env.config.validateExhaustiveEffectDependencies === false) {
163 + return;
164 }
149 - if (a.path.length !== b.path.length) {
150 - // if a's path is shorter this returns a negative, sorting a first
151 - return a.path.length - b.path.length;
165 + if (DEBUG) {
166 + console.log(Array.from(inferred, printInferredDependency));
167 + console.log(Array.from(manual, printInferredDependency));
168 }
153 - for (let i = 0; i < a.path.length; i++) {
154 - const aProperty = a.path[i];
155 - const bProperty = b.path[i];
156 - const aOptional = aProperty.optional ? 0 : 1;
157 - const bOptional = bProperty.optional ? 0 : 1;
158 - if (aOptional !== bOptional) {
159 - // sort non-optionals first
160 - return aOptional - bOptional;
161 - } else if (aProperty.property !== bProperty.property) {
162 - return String(aProperty.property).localeCompare(
163 - String(bProperty.property),
164 - );
169 + const manualDeps: Array<ManualMemoDependency> = [];
170 + for (const dep of manual) {
171 + if (dep.kind === 'Local') {
172 + manualDeps.push({
173 + root: {
174 + kind: 'NamedLocal',
175 + constant: false,
176 + value: {
177 + effect: Effect.Read,
178 + identifier: dep.identifier,
179 + kind: 'Identifier',
180 + loc: dep.loc,
181 + reactive: reactive.has(dep.identifier.id),
182 + },
183 + },
184 + path: dep.path,
185 + loc: dep.loc,
186 + });
187 + } else {
188 + manualDeps.push({
189 + root: {
190 + kind: 'Global',
191 + identifierName: dep.binding.name,
192 + },
193 + path: [],
194 + loc: GeneratedSource,
195 + });
196 }
197 }
167 - return 0;
168 - } else {
169 - const aName =
170 - a.kind === 'Global' ? a.binding.name : a.identifier.name?.value;
171 - const bName =
172 - b.kind === 'Global' ? b.binding.name : b.identifier.name?.value;
173 - if (aName != null && bName != null) {
174 - return aName.localeCompare(bName);
175 - }
176 - return 0;
177 - }
178 - });
179 - // remove redundant inferred dependencies
180 - retainWhere(inferred, (dep, ix) => {
181 - const match = inferred.findIndex(prevDep => {
182 - return (
183 - isEqualTemporary(prevDep, dep) ||
184 - (prevDep.kind === 'Local' &&
185 - dep.kind === 'Local' &&
186 - prevDep.identifier.id === dep.identifier.id &&
187 - isSubPath(prevDep.path, dep.path))
198 + const diagnostic = validateDependencies(
199 + Array.from(inferred),
200 + manualDeps,
201 + reactive,
202 + manualMemoLoc,
203 + ErrorCategory.EffectExhaustiveDependencies,
204 );
189 - });
190 - // only retain entries that don't have a prior match
191 - return match === -1 || match >= ix;
192 - });
193 - // Validate that all manual dependencies belong there
194 - if (DEBUG) {
195 - console.log('manual');
196 - console.log(
197 - (startMemo.deps ?? [])
198 - .map(x => ' ' + printManualMemoDependency(x))
199 - .join('\n'),
200 - );
201 - console.log('inferred');
202 - console.log(
203 - inferred.map(x => ' ' + printInferredDependency(x)).join('\n'),
205 + if (diagnostic != null) {
206 + error.pushDiagnostic(diagnostic);
207 + }
208 + },
209 + },
210 + false, // isFunctionExpression
211 + );
212 + return error.asResult();
213 +}
214 +
215 +function validateDependencies(
216 + inferred: Array<InferredDependency>,
217 + manualDependencies: Array<ManualMemoDependency>,
218 + reactive: Set<IdentifierId>,
219 + manualMemoLoc: SourceLocation | null,
220 + category:
221 + | ErrorCategory.MemoDependencies
222 + | ErrorCategory.EffectExhaustiveDependencies,
223 +): CompilerDiagnostic | null {
224 + // Sort dependencies by name and path, with shorter/non-optional paths first
225 + inferred.sort((a, b) => {
226 + if (a.kind === 'Global' && b.kind == 'Global') {
227 + return a.binding.name.localeCompare(b.binding.name);
228 + } else if (a.kind == 'Local' && b.kind == 'Local') {
229 + CompilerError.simpleInvariant(
230 + a.identifier.name != null &&
231 + a.identifier.name.kind === 'named' &&
232 + b.identifier.name != null &&
233 + b.identifier.name.kind === 'named',
234 + {
235 + reason: 'Expected dependencies to be named variables',
236 + loc: a.loc,
237 + },
238 );
205 - }
206 - const manualDependencies = startMemo.deps ?? [];
207 - const matched: Set<ManualMemoDependency> = new Set();
208 - const missing: Array<Extract<InferredDependency, {kind: 'Local'}>> = [];
209 - const extra: Array<ManualMemoDependency> = [];
210 - for (const inferredDependency of inferred) {
211 - if (inferredDependency.kind === 'Global') {
212 - for (const manualDependency of manualDependencies) {
213 - if (
214 - manualDependency.root.kind === 'Global' &&
215 - manualDependency.root.identifierName ===
216 - inferredDependency.binding.name
217 - ) {
218 - matched.add(manualDependency);
219 - extra.push(manualDependency);
220 - }
239 + if (a.identifier.id !== b.identifier.id) {
240 + return a.identifier.name.value.localeCompare(b.identifier.name.value);
241 + }
242 + if (a.path.length !== b.path.length) {
243 + // if a's path is shorter this returns a negative, sorting a first
244 + return a.path.length - b.path.length;
245 + }
246 + for (let i = 0; i < a.path.length; i++) {
247 + const aProperty = a.path[i];
248 + const bProperty = b.path[i];
249 + const aOptional = aProperty.optional ? 0 : 1;
250 + const bOptional = bProperty.optional ? 0 : 1;
251 + if (aOptional !== bOptional) {
252 + // sort non-optionals first
253 + return aOptional - bOptional;
254 + } else if (aProperty.property !== bProperty.property) {
255 + return String(aProperty.property).localeCompare(
256 + String(bProperty.property),
257 + );
258 }
222 - continue;
259 }
224 - CompilerError.simpleInvariant(inferredDependency.kind === 'Local', {
225 - reason: 'Unexpected function dependency',
226 - loc: value.loc,
227 - });
228 - let hasMatchingManualDependency = false;
260 + return 0;
261 + } else {
262 + const aName =
263 + a.kind === 'Global' ? a.binding.name : a.identifier.name?.value;
264 + const bName =
265 + b.kind === 'Global' ? b.binding.name : b.identifier.name?.value;
266 + if (aName != null && bName != null) {
267 + return aName.localeCompare(bName);
268 + }
269 + return 0;
270 + }
271 + });
272 + // remove redundant inferred dependencies
273 + retainWhere(inferred, (dep, ix) => {
274 + const match = inferred.findIndex(prevDep => {
275 + return (
276 + isEqualTemporary(prevDep, dep) ||
277 + (prevDep.kind === 'Local' &&
278 + dep.kind === 'Local' &&
279 + prevDep.identifier.id === dep.identifier.id &&
280 + isSubPath(prevDep.path, dep.path))
281 + );
282 + });
283 + // only retain entries that don't have a prior match
284 + return match === -1 || match >= ix;
285 + });
286 + // Validate that all manual dependencies belong there
287 + if (DEBUG) {
288 + console.log('manual');
289 + console.log(
290 + manualDependencies
291 + .map(x => ' ' + printManualMemoDependency(x))
292 + .join('\n'),
293 + );
294 + console.log('inferred');
295 + console.log(
296 + inferred.map(x => ' ' + printInferredDependency(x)).join('\n'),
297 + );
298 + }
299 + const matched: Set<ManualMemoDependency> = new Set();
300 + const missing: Array<Extract<InferredDependency, {kind: 'Local'}>> = [];
301 + const extra: Array<ManualMemoDependency> = [];
302 + for (const inferredDependency of inferred) {
303 + if (inferredDependency.kind === 'Global') {
304 for (const manualDependency of manualDependencies) {
305 if (
231 - manualDependency.root.kind === 'NamedLocal' &&
232 - manualDependency.root.value.identifier.id ===
233 - inferredDependency.identifier.id &&
234 - (areEqualPaths(manualDependency.path, inferredDependency.path) ||
235 - isSubPathIgnoringOptionals(
236 - manualDependency.path,
237 - inferredDependency.path,
238 - ))
306 + manualDependency.root.kind === 'Global' &&
307 + manualDependency.root.identifierName ===
308 + inferredDependency.binding.name
309 ) {
240 - hasMatchingManualDependency = true;
310 matched.add(manualDependency);
311 + extra.push(manualDependency);
312 }
313 }
314 + continue;
315 + }
316 + CompilerError.simpleInvariant(inferredDependency.kind === 'Local', {
317 + reason: 'Unexpected function dependency',
318 + loc: inferredDependency.loc,
319 + });
320 + /**
321 + * Skip effect event functions as they are not valid dependencies
322 + */
323 + if (isEffectEventFunctionType(inferredDependency.identifier)) {
324 + continue;
325 + }
326 + let hasMatchingManualDependency = false;
327 + for (const manualDependency of manualDependencies) {
328 if (
245 - hasMatchingManualDependency ||
246 - isOptionalDependency(inferredDependency, reactive)
329 + manualDependency.root.kind === 'NamedLocal' &&
330 + manualDependency.root.value.identifier.id ===
331 + inferredDependency.identifier.id &&
332 + (areEqualPaths(manualDependency.path, inferredDependency.path) ||
333 + isSubPathIgnoringOptionals(
334 + manualDependency.path,
335 + inferredDependency.path,
336 + ))
337 ) {
248 - continue;
338 + hasMatchingManualDependency = true;
339 + matched.add(manualDependency);
340 }
250 - missing.push(inferredDependency);
341 + }
342 + if (
343 + hasMatchingManualDependency ||
344 + isOptionalDependency(inferredDependency, reactive)
345 + ) {
346 + continue;
347 }
348
253 - for (const dep of startMemo.deps ?? []) {
254 - if (matched.has(dep)) {
255 - continue;
256 - }
257 - if (dep.root.kind === 'NamedLocal' && dep.root.constant) {
258 - CompilerError.simpleInvariant(
259 - !dep.root.value.reactive &&
260 - isPrimitiveType(dep.root.value.identifier),
261 - {
262 - reason: 'Expected constant-folded dependency to be non-reactive',
263 - loc: dep.root.value.loc,
264 - },
265 - );
266 - /*
267 - * Constant primitives can get constant-folded, which means we won't
268 - * see a LoadLocal for the value within the memo function.
269 - */
270 - continue;
271 - }
272 - extra.push(dep);
349 + missing.push(inferredDependency);
350 + }
351 +
352 + for (const dep of manualDependencies) {
353 + if (matched.has(dep)) {
354 + continue;
355 + }
356 + if (dep.root.kind === 'NamedLocal' && dep.root.constant) {
357 + CompilerError.simpleInvariant(
358 + !dep.root.value.reactive && isPrimitiveType(dep.root.value.identifier),
359 + {
360 + reason: 'Expected constant-folded dependency to be non-reactive',
361 + loc: dep.root.value.loc,
362 + },
363 + );
364 + /*
365 + * Constant primitives can get constant-folded, which means we won't
366 + * see a LoadLocal for the value within the memo function.
367 + */
368 + continue;
369 }
370 + extra.push(dep);
371 + }
372
275 - if (missing.length !== 0 || extra.length !== 0) {
276 - let suggestion: CompilerSuggestion | null = null;
277 - if (startMemo.depsLoc != null && typeof startMemo.depsLoc !== 'symbol') {
278 - suggestion = {
279 - description: 'Update dependencies',
280 - range: [startMemo.depsLoc.start.index, startMemo.depsLoc.end.index],
281 - op: CompilerSuggestionOperation.Replace,
282 - text: `[${inferred
283 - .filter(
284 - dep =>
285 - dep.kind === 'Local' && !isOptionalDependency(dep, reactive),
286 - )
287 - .map(printInferredDependency)
288 - .join(', ')}]`,
289 - };
373 + if (missing.length !== 0 || extra.length !== 0) {
374 + let suggestion: CompilerSuggestion | null = null;
375 + if (manualMemoLoc != null && typeof manualMemoLoc !== 'symbol') {
376 + suggestion = {
377 + description: 'Update dependencies',
378 + range: [manualMemoLoc.start.index, manualMemoLoc.end.index],
379 + op: CompilerSuggestionOperation.Replace,
380 + text: `[${inferred
381 + .filter(
382 + dep =>
383 + dep.kind === 'Local' &&
384 + !isOptionalDependency(dep, reactive) &&
385 + !isEffectEventFunctionType(dep.identifier),
386 + )
387 + .map(printInferredDependency)
388 + .join(', ')}]`,
389 + };
390 + }
391 + const diagnostic = createDiagnostic(category, missing, extra, suggestion);
392 + for (const dep of missing) {
393 + let reactiveStableValueHint = '';
394 + if (isStableType(dep.identifier)) {
395 + reactiveStableValueHint =
396 + '. Refs, setState functions, and other "stable" values generally do not need to be added ' +
397 + 'as dependencies, but this variable may change over time to point to different values';
398 }
291 - const diagnostic = CompilerDiagnostic.create({
292 - category: ErrorCategory.MemoDependencies,
293 - reason: 'Found missing/extra memoization dependencies',
294 - description: [
295 - missing.length !== 0
296 - ? 'Missing dependencies can cause a value to update less often than it should, ' +
297 - 'resulting in stale UI'
298 - : null,
299 - extra.length !== 0
300 - ? 'Extra dependencies can cause a value to update more often than it should, ' +
301 - 'resulting in performance problems such as excessive renders or effects firing too often'
302 - : null,
303 - ]
304 - .filter(Boolean)
305 - .join('. '),
306 - suggestions: suggestion != null ? [suggestion] : null,
399 + diagnostic.withDetails({
400 + kind: 'error',
401 + message: `Missing dependency \`${printInferredDependency(dep)}\`${reactiveStableValueHint}`,
402 + loc: dep.loc,
403 });
308 - for (const dep of missing) {
309 - let reactiveStableValueHint = '';
310 - if (isStableType(dep.identifier)) {
311 - reactiveStableValueHint =
312 - '. Refs, setState functions, and other "stable" values generally do not need to be added ' +
313 - 'as dependencies, but this variable may change over time to point to different values';
314 - }
404 + }
405 + for (const dep of extra) {
406 + if (dep.root.kind === 'Global') {
407 diagnostic.withDetails({
408 kind: 'error',
317 - message: `Missing dependency \`${printInferredDependency(dep)}\`${reactiveStableValueHint}`,
318 - loc: dep.loc,
409 + message:
410 + `Unnecessary dependency \`${printManualMemoDependency(dep)}\`. ` +
411 + 'Values declared outside of a component/hook should not be listed as ' +
412 + 'dependencies as the component will not re-render if they change',
413 + loc: dep.loc ?? manualMemoLoc,
414 });
320 - }
321 - for (const dep of extra) {
322 - if (dep.root.kind === 'Global') {
415 + } else {
416 + const root = dep.root.value;
417 + const matchingInferred = inferred.find(
418 + (
419 + inferredDep,
420 + ): inferredDep is Extract<InferredDependency, {kind: 'Local'}> => {
421 + return (
422 + inferredDep.kind === 'Local' &&
423 + inferredDep.identifier.id === root.identifier.id &&
424 + isSubPathIgnoringOptionals(inferredDep.path, dep.path)
425 + );
426 + },
427 + );
428 + if (
429 + matchingInferred != null &&
430 + isEffectEventFunctionType(matchingInferred.identifier)
431 + ) {
432 diagnostic.withDetails({
433 kind: 'error',
434 message:
326 - `Unnecessary dependency \`${printManualMemoDependency(dep)}\`. ` +
327 - 'Values declared outside of a component/hook should not be listed as ' +
328 - 'dependencies as the component will not re-render if they change',
329 - loc: dep.loc ?? startMemo.depsLoc ?? value.loc,
435 + `Functions returned from \`useEffectEvent\` must not be included in the dependency array. ` +
436 + `Remove \`${printManualMemoDependency(dep)}\` from the dependencies.`,
437 + loc: dep.loc ?? manualMemoLoc,
438 + });
439 + } else if (
440 + matchingInferred != null &&
441 + !isOptionalDependency(matchingInferred, reactive)
442 + ) {
443 + diagnostic.withDetails({
444 + kind: 'error',
445 + message:
446 + `Overly precise dependency \`${printManualMemoDependency(dep)}\`, ` +
447 + `use \`${printInferredDependency(matchingInferred)}\` instead`,
448 + loc: dep.loc ?? manualMemoLoc,
449 });
331 - error.pushDiagnostic(diagnostic);
450 } else {
333 - const root = dep.root.value;
334 - const matchingInferred = inferred.find(
335 - (
336 - inferredDep,
337 - ): inferredDep is Extract<InferredDependency, {kind: 'Local'}> => {
338 - return (
339 - inferredDep.kind === 'Local' &&
340 - inferredDep.identifier.id === root.identifier.id &&
341 - isSubPathIgnoringOptionals(inferredDep.path, dep.path)
342 - );
343 - },
344 - );
345 - if (
346 - matchingInferred != null &&
347 - !isOptionalDependency(matchingInferred, reactive)
348 - ) {
349 - diagnostic.withDetails({
350 - kind: 'error',
351 - message:
352 - `Overly precise dependency \`${printManualMemoDependency(dep)}\`, ` +
353 - `use \`${printInferredDependency(matchingInferred)}\` instead`,
354 - loc: dep.loc ?? startMemo.depsLoc ?? value.loc,
355 - });
356 - } else {
357 - /**
358 - * Else this dependency doesn't correspond to anything referenced in the memo function,
359 - * or is an optional dependency so we don't want to suggest adding it
360 - */
361 - diagnostic.withDetails({
362 - kind: 'error',
363 - message: `Unnecessary dependency \`${printManualMemoDependency(dep)}\``,
364 - loc: dep.loc ?? startMemo.depsLoc ?? value.loc,
365 - });
366 - }
451 + /**
452 + * Else this dependency doesn't correspond to anything referenced in the memo function,
453 + * or is an optional dependency so we don't want to suggest adding it
454 + */
455 + diagnostic.withDetails({
456 + kind: 'error',
457 + message: `Unnecessary dependency \`${printManualMemoDependency(dep)}\``,
458 + loc: dep.loc ?? manualMemoLoc,
459 + });
460 }
461 }
369 - if (suggestion != null) {
370 - diagnostic.withDetails({
371 - kind: 'hint',
372 - message: `Inferred dependencies: \`${suggestion.text}\``,
373 - });
374 - }
375 - error.pushDiagnostic(diagnostic);
462 }
377 -
378 - dependencies.clear();
379 - locals.clear();
380 - startMemo = null;
463 + if (suggestion != null) {
464 + diagnostic.withDetails({
465 + kind: 'hint',
466 + message: `Inferred dependencies: \`${suggestion.text}\``,
467 + });
468 + }
469 + return diagnostic;
470 }
382 -
383 - collectDependencies(
384 - fn,
385 - temporaries,
386 - {
387 - onStartMemoize,
388 - onFinishMemoize,
389 - },
390 - false, // isFunctionExpression
391 - );
392 - return error.asResult();
471 + return null;
472 }
473
474 function addDependency(
@@ -397,7 +476,7 @@ function addDependency(
476 dependencies: Set<InferredDependency>,
477 locals: Set<IdentifierId>,
478 ): void {
400 - if (dep.kind === 'Function') {
479 + if (dep.kind === 'Aggregate') {
480 for (const x of dep.dependencies) {
481 addDependency(x, dependencies, locals);
482 }
@@ -480,9 +559,14 @@ function collectDependencies(
559 dependencies: Set<InferredDependency>,
560 locals: Set<IdentifierId>,
561 ) => void;
562 + onEffect: (
563 + inferred: Set<InferredDependency>,
564 + manual: Set<InferredDependency>,
565 + manualMemoLoc: SourceLocation | null,
566 + ) => void;
567 } | null,
568 isFunctionExpression: boolean,
485 -): Extract<Temporary, {kind: 'Function'}> {
569 +): Extract<Temporary, {kind: 'Aggregate'}> {
570 const optionals = findOptionalPlaces(fn);
571 if (DEBUG) {
572 console.log(prettyFormat(optionals));
@@ -501,25 +585,25 @@ function collectDependencies(
585 }
586 for (const block of fn.body.blocks.values()) {
587 for (const phi of block.phis) {
504 - let deps: Array<Temporary> | null = null;
588 + const deps: Array<InferredDependency> = [];
589 for (const operand of phi.operands.values()) {
590 const dep = temporaries.get(operand.identifier.id);
591 if (dep == null) {
592 continue;
593 }
510 - if (deps == null) {
511 - deps = [dep];
594 + if (dep.kind === 'Aggregate') {
595 + deps.push(...dep.dependencies);
596 } else {
597 deps.push(dep);
598 }
599 }
516 - if (deps == null) {
600 + if (deps.length === 0) {
601 continue;
602 } else if (deps.length === 1) {
603 temporaries.set(phi.place.identifier.id, deps[0]!);
604 } else {
605 temporaries.set(phi.place.identifier.id, {
522 - kind: 'Function',
606 + kind: 'Aggregate',
607 dependencies: new Set(deps),
608 });
609 }
@@ -537,9 +621,6 @@ function collectDependencies(
621 }
622 case 'LoadContext':
623 case 'LoadLocal': {
540 - if (locals.has(value.place.identifier.id)) {
541 - break;
542 - }
624 const temp = temporaries.get(value.place.identifier.id);
625 if (temp != null) {
626 if (temp.kind === 'Local') {
@@ -548,6 +629,9 @@ function collectDependencies(
629 } else {
630 temporaries.set(lvalue.identifier.id, temp);
631 }
632 + if (locals.has(value.place.identifier.id)) {
633 + locals.add(lvalue.identifier.id);
634 + }
635 }
636 break;
637 }
@@ -683,10 +767,55 @@ function collectDependencies(
767 }
768 break;
769 }
770 + case 'ArrayExpression': {
771 + const arrayDeps: Set<InferredDependency> = new Set();
772 + for (const item of value.elements) {
773 + if (item.kind === 'Hole') {
774 + continue;
775 + }
776 + const place = item.kind === 'Identifier' ? item : item.place;
777 + // Visit with alternative deps/locals to record manual dependencies
778 + visitCandidateDependency(place, temporaries, arrayDeps, new Set());
779 + // Visit normally to propagate inferred dependencies upward
780 + visit(place);
781 + }
782 + temporaries.set(lvalue.identifier.id, {
783 + kind: 'Aggregate',
784 + dependencies: arrayDeps,
785 + loc: value.loc,
786 + });
787 + break;
788 + }
789 + case 'CallExpression':
790 case 'MethodCall': {
791 + const receiver =
792 + value.kind === 'CallExpression' ? value.callee : value.property;
793 +
794 + const onEffect = callbacks?.onEffect;
795 + if (onEffect != null && isEffectHook(receiver.identifier)) {
796 + const [fn, deps] = value.args;
797 + if (fn?.kind === 'Identifier' && deps?.kind === 'Identifier') {
798 + const fnDeps = temporaries.get(fn.identifier.id);
799 + const manualDeps = temporaries.get(deps.identifier.id);
800 + if (
801 + fnDeps?.kind === 'Aggregate' &&
802 + manualDeps?.kind === 'Aggregate'
803 + ) {
804 + onEffect(
805 + fnDeps.dependencies,
806 + manualDeps.dependencies,
807 + manualDeps.loc ?? null,
808 + );
809 + }
810 + }
811 + }
812 +
813 // Ignore the method itself
814 for (const operand of eachInstructionValueOperand(value)) {
689 - if (operand.identifier.id === value.property.identifier.id) {
815 + if (
816 + value.kind === 'MethodCall' &&
817 + operand.identifier.id === value.property.identifier.id
818 + ) {
819 continue;
820 }
821 visit(operand);
@@ -710,7 +839,7 @@ function collectDependencies(
839 visit(operand);
840 }
841 }
713 - return {kind: 'Function', dependencies};
842 + return {kind: 'Aggregate', dependencies};
843 }
844
845 function printInferredDependency(dep: InferredDependency): string {
@@ -748,7 +877,7 @@ function printManualMemoDependency(dep: ManualMemoDependency): string {
877
878 function isEqualTemporary(a: Temporary, b: Temporary): boolean {
879 switch (a.kind) {
751 - case 'Function': {
880 + case 'Aggregate': {
881 return false;
882 }
883 case 'Global': {
@@ -773,7 +902,11 @@ type Temporary =
902 context: boolean;
903 loc: SourceLocation;
904 }
776 - | {kind: 'Function'; dependencies: Set<Temporary>};
905 + | {
906 + kind: 'Aggregate';
907 + dependencies: Set<InferredDependency>;
908 + loc?: SourceLocation;
909 + };
910 type InferredDependency = Extract<Temporary, {kind: 'Local' | 'Global'}>;
911
912 function collectReactiveIdentifiersHIR(fn: HIRFunction): Set<IdentifierId> {
@@ -888,3 +1021,64 @@ function isOptionalDependency(
1021 isPrimitiveType(inferredDependency.identifier))
1022 );
1023 }
1024 +
1025 +function createDiagnostic(
1026 + category:
1027 + | ErrorCategory.MemoDependencies
1028 + | ErrorCategory.EffectExhaustiveDependencies,
1029 + missing: Array<InferredDependency>,
1030 + extra: Array<ManualMemoDependency>,
1031 + suggestion: CompilerSuggestion | null,
1032 +): CompilerDiagnostic {
1033 + let reason: string;
1034 + let description: string;
1035 +
1036 + function joinMissingExtraDetail(
1037 + missingString: string,
1038 + extraString: string,
1039 + joinStr: string,
1040 + ): string {
1041 + return [
1042 + missing.length !== 0 ? missingString : null,
1043 + extra.length !== 0 ? extraString : null,
1044 + ]
1045 + .filter(Boolean)
1046 + .join(joinStr);
1047 + }
1048 +
1049 + switch (category) {
1050 + case ErrorCategory.MemoDependencies: {
1051 + reason = `Found ${joinMissingExtraDetail('missing', 'extra', '/')} memoization dependencies`;
1052 + description = joinMissingExtraDetail(
1053 + 'Missing dependencies can cause a value to update less often than it should, resulting in stale UI',
1054 + 'Extra dependencies can cause a value to update more often than it should, resulting in performance' +
1055 + ' problems such as excessive renders or effects firing too often',
1056 + '. ',
1057 + );
1058 + break;
1059 + }
1060 + case ErrorCategory.EffectExhaustiveDependencies: {
1061 + reason = `Found ${joinMissingExtraDetail('missing', 'extra', '/')} effect dependencies`;
1062 + description = joinMissingExtraDetail(
1063 + 'Missing dependencies can cause an effect to fire less often than it should',
1064 + 'Extra dependencies can cause an effect to fire more often than it should, resulting' +
1065 + ' in performance problems such as excessive renders and side effects',
1066 + '. ',
1067 + );
1068 + break;
1069 + }
1070 + default: {
1071 + CompilerError.simpleInvariant(false, {
1072 + reason: `Unexpected error category: ${category}`,
1073 + loc: GeneratedSource,
1074 + });
1075 + }
1076 + }
1077 +
1078 + return CompilerDiagnostic.create({
1079 + category,
1080 + reason,
1081 + description,
1082 + suggestions: suggestion != null ? [suggestion] : null,
1083 + });
1084 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.exhaustive-deps-effect-events.expect.md new
+86
@@ -0,0 +1,86 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateExhaustiveEffectDependencies
6 +import {useEffect, useEffectEvent} from 'react';
7 +
8 +function Component({x, y, z}) {
9 + const effectEvent = useEffectEvent(() => {
10 + log(x);
11 + });
12 +
13 + const effectEvent2 = useEffectEvent(z => {
14 + log(y, z);
15 + });
16 +
17 + // error - do not include effect event in deps
18 + useEffect(() => {
19 + effectEvent();
20 + }, [effectEvent]);
21 +
22 + // error - do not include effect event in deps
23 + useEffect(() => {
24 + effectEvent2(z);
25 + }, [effectEvent2, z]);
26 +
27 + // error - do not include effect event captured values in deps
28 + useEffect(() => {
29 + effectEvent2(z);
30 + }, [y, z]);
31 +}
32 +
33 +```
34 +
35 +
36 +## Error
37 +
38 +```
39 +Found 3 errors:
40 +
41 +Error: Found extra effect dependencies
42 +
43 +Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
44 +
45 +error.exhaustive-deps-effect-events.ts:16:6
46 + 14 | useEffect(() => {
47 + 15 | effectEvent();
48 +> 16 | }, [effectEvent]);
49 + | ^^^^^^^^^^^ Functions returned from `useEffectEvent` must not be included in the dependency array. Remove `effectEvent` from the dependencies.
50 + 17 |
51 + 18 | // error - do not include effect event in deps
52 + 19 | useEffect(() => {
53 +
54 +Inferred dependencies: `[]`
55 +
56 +Error: Found extra effect dependencies
57 +
58 +Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
59 +
60 +error.exhaustive-deps-effect-events.ts:21:6
61 + 19 | useEffect(() => {
62 + 20 | effectEvent2(z);
63 +> 21 | }, [effectEvent2, z]);
64 + | ^^^^^^^^^^^^ Functions returned from `useEffectEvent` must not be included in the dependency array. Remove `effectEvent2` from the dependencies.
65 + 22 |
66 + 23 | // error - do not include effect event captured values in deps
67 + 24 | useEffect(() => {
68 +
69 +Inferred dependencies: `[z]`
70 +
71 +Error: Found extra effect dependencies
72 +
73 +Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
74 +
75 +error.exhaustive-deps-effect-events.ts:26:6
76 + 24 | useEffect(() => {
77 + 25 | effectEvent2(z);
78 +> 26 | }, [y, z]);
79 + | ^ Unnecessary dependency `y`
80 + 27 | }
81 + 28 |
82 +
83 +Inferred dependencies: `[z]`
84 +```
85 +
86 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.exhaustive-deps-effect-events.js new
+27
@@ -0,0 +1,27 @@
1 +// @validateExhaustiveEffectDependencies
2 +import {useEffect, useEffectEvent} from 'react';
3 +
4 +function Component({x, y, z}) {
5 + const effectEvent = useEffectEvent(() => {
6 + log(x);
7 + });
8 +
9 + const effectEvent2 = useEffectEvent(z => {
10 + log(y, z);
11 + });
12 +
13 + // error - do not include effect event in deps
14 + useEffect(() => {
15 + effectEvent();
16 + }, [effectEvent]);
17 +
18 + // error - do not include effect event in deps
19 + useEffect(() => {
20 + effectEvent2(z);
21 + }, [effectEvent2, z]);
22 +
23 + // error - do not include effect event captured values in deps
24 + useEffect(() => {
25 + effectEvent2(z);
26 + }, [y, z]);
27 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-dep-on-ref-current-value.expect.md
+1 -1
@@ -21,7 +21,7 @@ function Component() {
21 ```
22 Found 1 error:
23
24 -Error: Found missing/extra memoization dependencies
24 +Error: Found extra memoization dependencies
25
26 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.
27
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps-disallow-unused-stable-types.expect.md
+1 -1
@@ -25,7 +25,7 @@ function Component() {
25 ```
26 Found 1 error:
27
28 -Error: Found missing/extra memoization dependencies
28 +Error: Found extra memoization dependencies
29
30 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.
31
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.expect.md
+3 -45
@@ -51,7 +51,7 @@ function Component({x, y, z}) {
51 ## Error
52
53 ```
54 -Found 5 errors:
54 +Found 4 errors:
55
56 Error: Found missing/extra memoization dependencies
57
@@ -101,49 +101,7 @@ error.invalid-exhaustive-deps.ts:17:6
101
102 Inferred dependencies: `[x?.y.z.a?.b]`
103
104 -Error: Found missing/extra memoization dependencies
105 -
106 -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.
107 -
108 -error.invalid-exhaustive-deps.ts:31:6
109 - 29 | return [];
110 - 30 | // error: unnecessary
111 -> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
112 - | ^ Unnecessary dependency `x`
113 - 32 | const ref1 = useRef(null);
114 - 33 | const ref2 = useRef(null);
115 - 34 | const ref = z ? ref1 : ref2;
116 -
117 -error.invalid-exhaustive-deps.ts:31:9
118 - 29 | return [];
119 - 30 | // error: unnecessary
120 -> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
121 - | ^^^ Unnecessary dependency `y.z`
122 - 32 | const ref1 = useRef(null);
123 - 33 | const ref2 = useRef(null);
124 - 34 | const ref = z ? ref1 : ref2;
125 -
126 -error.invalid-exhaustive-deps.ts:31:14
127 - 29 | return [];
128 - 30 | // error: unnecessary
129 -> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
130 - | ^^^^^^^ Unnecessary dependency `z?.y?.a`
131 - 32 | const ref1 = useRef(null);
132 - 33 | const ref2 = useRef(null);
133 - 34 | const ref = z ? ref1 : ref2;
134 -
135 -error.invalid-exhaustive-deps.ts:31:23
136 - 29 | return [];
137 - 30 | // error: unnecessary
138 -> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
139 - | ^^^^^^^^^^^^^ 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
140 - 32 | const ref1 = useRef(null);
141 - 33 | const ref2 = useRef(null);
142 - 34 | const ref = z ? ref1 : ref2;
143 -
144 -Inferred dependencies: `[]`
145 -
146 -Error: Found missing/extra memoization dependencies
104 +Error: Found extra memoization dependencies
105
106 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.
107
@@ -185,7 +143,7 @@ error.invalid-exhaustive-deps.ts:31:23
143
144 Inferred dependencies: `[]`
145
188 -Error: Found missing/extra memoization dependencies
146 +Error: Found missing memoization dependencies
147
148 Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
149
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps.expect.md new
+116
@@ -0,0 +1,116 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateExhaustiveEffectDependencies
6 +import {useEffect} from 'react';
7 +
8 +function Component({x, y, z}) {
9 + // error: missing dep - x
10 + useEffect(() => {
11 + log(x);
12 + }, []);
13 +
14 + // error: extra dep - y
15 + useEffect(() => {
16 + log(x);
17 + }, [x, y]);
18 +
19 + // error: missing dep - z; extra dep - y
20 + useEffect(() => {
21 + log(x, z);
22 + }, [x, y]);
23 +
24 + // error: missing dep x
25 + useEffect(() => {
26 + log(x);
27 + }, [x.y]);
28 +}
29 +
30 +```
31 +
32 +
33 +## Error
34 +
35 +```
36 +Found 4 errors:
37 +
38 +Error: Found missing effect dependencies
39 +
40 +Missing dependencies can cause an effect to fire less often than it should.
41 +
42 +error.invalid-exhaustive-effect-deps.ts:7:8
43 + 5 | // error: missing dep - x
44 + 6 | useEffect(() => {
45 +> 7 | log(x);
46 + | ^ Missing dependency `x`
47 + 8 | }, []);
48 + 9 |
49 + 10 | // error: extra dep - y
50 +
51 +Inferred dependencies: `[x]`
52 +
53 +Error: Found extra effect dependencies
54 +
55 +Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
56 +
57 +error.invalid-exhaustive-effect-deps.ts:13:9
58 + 11 | useEffect(() => {
59 + 12 | log(x);
60 +> 13 | }, [x, y]);
61 + | ^ Unnecessary dependency `y`
62 + 14 |
63 + 15 | // error: missing dep - z; extra dep - y
64 + 16 | useEffect(() => {
65 +
66 +Inferred dependencies: `[x]`
67 +
68 +Error: Found missing/extra effect dependencies
69 +
70 +Missing dependencies can cause an effect to fire less often than it should. Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
71 +
72 +error.invalid-exhaustive-effect-deps.ts:17:11
73 + 15 | // error: missing dep - z; extra dep - y
74 + 16 | useEffect(() => {
75 +> 17 | log(x, z);
76 + | ^ Missing dependency `z`
77 + 18 | }, [x, y]);
78 + 19 |
79 + 20 | // error: missing dep x
80 +
81 +error.invalid-exhaustive-effect-deps.ts:18:9
82 + 16 | useEffect(() => {
83 + 17 | log(x, z);
84 +> 18 | }, [x, y]);
85 + | ^ Unnecessary dependency `y`
86 + 19 |
87 + 20 | // error: missing dep x
88 + 21 | useEffect(() => {
89 +
90 +Inferred dependencies: `[x, z]`
91 +
92 +Error: Found missing/extra effect dependencies
93 +
94 +Missing dependencies can cause an effect to fire less often than it should. Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
95 +
96 +error.invalid-exhaustive-effect-deps.ts:22:8
97 + 20 | // error: missing dep x
98 + 21 | useEffect(() => {
99 +> 22 | log(x);
100 + | ^ Missing dependency `x`
101 + 23 | }, [x.y]);
102 + 24 | }
103 + 25 |
104 +
105 +error.invalid-exhaustive-effect-deps.ts:23:6
106 + 21 | useEffect(() => {
107 + 22 | log(x);
108 +> 23 | }, [x.y]);
109 + | ^^^ Overly precise dependency `x.y`, use `x` instead
110 + 24 | }
111 + 25 |
112 +
113 +Inferred dependencies: `[x]`
114 +```
115 +
116 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps.js new
+24
@@ -0,0 +1,24 @@
1 +// @validateExhaustiveEffectDependencies
2 +import {useEffect} from 'react';
3 +
4 +function Component({x, y, z}) {
5 + // error: missing dep - x
6 + useEffect(() => {
7 + log(x);
8 + }, []);
9 +
10 + // error: extra dep - y
11 + useEffect(() => {
12 + log(x);
13 + }, [x, y]);
14 +
15 + // error: missing dep - z; extra dep - y
16 + useEffect(() => {
17 + log(x, z);
18 + }, [x, y]);
19 +
20 + // error: missing dep x
21 + useEffect(() => {
22 + log(x);
23 + }, [x.y]);
24 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-inner-function.expect.md
+1 -1
@@ -26,7 +26,7 @@ function useHook() {
26 ```
27 Found 1 error:
28
29 -Error: Found missing/extra memoization dependencies
29 +Error: Found missing memoization dependencies
30
31 Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
32
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-unmemoized.expect.md
+1 -1
@@ -24,7 +24,7 @@ function useHook() {
24 ```
25 Found 1 error:
26
27 -Error: Found missing/extra memoization dependencies
27 +Error: Found missing memoization dependencies
28
29 Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
30
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep.expect.md
+1 -1
@@ -21,7 +21,7 @@ function useHook() {
21 ```
22 Found 1 error:
23
24 -Error: Found missing/extra memoization dependencies
24 +Error: Found missing memoization dependencies
25
26 Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
27
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.sketchy-code-exhaustive-deps.expect.md
+1 -1
@@ -25,7 +25,7 @@ function Component() {
25 ```
26 Found 1 error:
27
28 -Error: Found missing/extra memoization dependencies
28 +Error: Found missing memoization dependencies
29
30 Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
31
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.expect.md
+53 -5
@@ -2,7 +2,7 @@
2 ## Input
3
4 ```javascript
5 -// @validateExhaustiveMemoizationDependencies
5 +// @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies
6 import {
7 useCallback,
8 useTransition,
@@ -11,6 +11,7 @@ import {
11 useActionState,
12 useRef,
13 useReducer,
14 + useEffect,
15 } from 'react';
16
17 function useFoo() {
@@ -21,6 +22,24 @@ function useFoo() {
22 const [v, dispatch] = useReducer(() => {}, null);
23 const [isPending, dispatchAction] = useActionState(() => {}, null);
24
25 + useEffect(() => {
26 + dispatch();
27 + startTransition(() => {});
28 + addOptimistic();
29 + setState(null);
30 + dispatchAction();
31 + ref.current = true;
32 + }, [
33 + // intentionally adding unnecessary deps on nonreactive stable values
34 + // to check that they're allowed
35 + dispatch,
36 + startTransition,
37 + addOptimistic,
38 + setState,
39 + dispatchAction,
40 + ref,
41 + ]);
42 +
43 return useCallback(() => {
44 dispatch();
45 startTransition(() => {});
@@ -50,7 +69,7 @@ export const FIXTURE_ENTRYPOINT = {
69 ## Code
70
71 ```javascript
53 -import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
72 +import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies
73 import {
74 useCallback,
75 useTransition,
@@ -59,10 +78,11 @@ import {
78 useActionState,
79 useRef,
80 useReducer,
81 + useEffect,
82 } from "react";
83
84 function useFoo() {
65 - const $ = _c(1);
85 + const $ = _c(3);
86 const [, setState] = useState();
87 const ref = useRef(null);
88 const [, startTransition] = useTransition();
@@ -70,6 +90,7 @@ function useFoo() {
90 const [, dispatch] = useReducer(_temp, null);
91 const [, dispatchAction] = useActionState(_temp2, null);
92 let t0;
93 + let t1;
94 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
95 t0 = () => {
96 dispatch();
@@ -79,12 +100,38 @@ function useFoo() {
100 dispatchAction();
101 ref.current = true;
102 };
103 + t1 = [
104 + dispatch,
105 + startTransition,
106 + addOptimistic,
107 + setState,
108 + dispatchAction,
109 + ref,
110 + ];
111 $[0] = t0;
112 + $[1] = t1;
113 } else {
114 t0 = $[0];
115 + t1 = $[1];
116 + }
117 + useEffect(t0, t1);
118 + let t2;
119 + if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
120 + t2 = () => {
121 + dispatch();
122 + startTransition(_temp4);
123 + addOptimistic();
124 + setState(null);
125 + dispatchAction();
126 + ref.current = true;
127 + };
128 + $[2] = t2;
129 + } else {
130 + t2 = $[2];
131 }
86 - return t0;
132 + return t2;
133 }
134 +function _temp4() {}
135 function _temp3() {}
136 function _temp2() {}
137 function _temp() {}
@@ -97,4 +144,5 @@ export const FIXTURE_ENTRYPOINT = {
144 ```
145
146 ### Eval output
100 -(kind: ok) "[[ function params=0 ]]"
\ No newline at end of file
147 +(kind: ok) "[[ function params=0 ]]"
148 +logs: ['An optimistic state update occurred outside a transition or action. To fix, move the update to an action, or wrap with startTransition.']
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.js
+20 -1
@@ -1,4 +1,4 @@
1 -// @validateExhaustiveMemoizationDependencies
1 +// @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies
2 import {
3 useCallback,
4 useTransition,
@@ -7,6 +7,7 @@ import {
7 useActionState,
8 useRef,
9 useReducer,
10 + useEffect,
11 } from 'react';
12
13 function useFoo() {
@@ -17,6 +18,24 @@ function useFoo() {
18 const [v, dispatch] = useReducer(() => {}, null);
19 const [isPending, dispatchAction] = useActionState(() => {}, null);
20
21 + useEffect(() => {
22 + dispatch();
23 + startTransition(() => {});
24 + addOptimistic();
25 + setState(null);
26 + dispatchAction();
27 + ref.current = true;
28 + }, [
29 + // intentionally adding unnecessary deps on nonreactive stable values
30 + // to check that they're allowed
31 + dispatch,
32 + startTransition,
33 + addOptimistic,
34 + setState,
35 + dispatchAction,
36 + ref,
37 + ]);
38 +
39 return useCallback(() => {
40 dispatch();
41 startTransition(() => {});
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-effect-events.expect.md new
+104
@@ -0,0 +1,104 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateExhaustiveEffectDependencies
6 +import {useEffect, useEffectEvent} from 'react';
7 +
8 +function Component({x, y, z}) {
9 + const effectEvent = useEffectEvent(() => {
10 + log(x);
11 + });
12 +
13 + const effectEvent2 = useEffectEvent(z => {
14 + log(y, z);
15 + });
16 +
17 + // ok - effectEvent not included in deps
18 + useEffect(() => {
19 + effectEvent();
20 + }, []);
21 +
22 + // ok - effectEvent2 not included in deps, z included
23 + useEffect(() => {
24 + effectEvent2(z);
25 + }, [z]);
26 +}
27 +
28 +```
29 +
30 +## Code
31 +
32 +```javascript
33 +import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveEffectDependencies
34 +import { useEffect, useEffectEvent } from "react";
35 +
36 +function Component(t0) {
37 + const $ = _c(12);
38 + const { x, y, z } = t0;
39 + let t1;
40 + if ($[0] !== x) {
41 + t1 = () => {
42 + log(x);
43 + };
44 + $[0] = x;
45 + $[1] = t1;
46 + } else {
47 + t1 = $[1];
48 + }
49 + const effectEvent = useEffectEvent(t1);
50 + let t2;
51 + if ($[2] !== y) {
52 + t2 = (z_0) => {
53 + log(y, z_0);
54 + };
55 + $[2] = y;
56 + $[3] = t2;
57 + } else {
58 + t2 = $[3];
59 + }
60 + const effectEvent2 = useEffectEvent(t2);
61 + let t3;
62 + if ($[4] !== effectEvent) {
63 + t3 = () => {
64 + effectEvent();
65 + };
66 + $[4] = effectEvent;
67 + $[5] = t3;
68 + } else {
69 + t3 = $[5];
70 + }
71 + let t4;
72 + if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
73 + t4 = [];
74 + $[6] = t4;
75 + } else {
76 + t4 = $[6];
77 + }
78 + useEffect(t3, t4);
79 + let t5;
80 + if ($[7] !== effectEvent2 || $[8] !== z) {
81 + t5 = () => {
82 + effectEvent2(z);
83 + };
84 + $[7] = effectEvent2;
85 + $[8] = z;
86 + $[9] = t5;
87 + } else {
88 + t5 = $[9];
89 + }
90 + let t6;
91 + if ($[10] !== z) {
92 + t6 = [z];
93 + $[10] = z;
94 + $[11] = t6;
95 + } else {
96 + t6 = $[11];
97 + }
98 + useEffect(t5, t6);
99 +}
100 +
101 +```
102 +
103 +### Eval output
104 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-effect-events.js new
+22
@@ -0,0 +1,22 @@
1 +// @validateExhaustiveEffectDependencies
2 +import {useEffect, useEffectEvent} from 'react';
3 +
4 +function Component({x, y, z}) {
5 + const effectEvent = useEffectEvent(() => {
6 + log(x);
7 + });
8 +
9 + const effectEvent2 = useEffectEvent(z => {
10 + log(y, z);
11 + });
12 +
13 + // ok - effectEvent not included in deps
14 + useEffect(() => {
15 + effectEvent();
16 + }, []);
17 +
18 + // ok - effectEvent2 not included in deps, z included
19 + useEffect(() => {
20 + effectEvent2(z);
21 + }, [z]);
22 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-unrelated-mutation-in-depslist.expect.md
+1 -1
@@ -32,7 +32,7 @@ function useFoo(input1) {
32 ```
33 Found 1 error:
34
35 -Error: Found missing/extra memoization dependencies
35 +Error: Found missing memoization dependencies
36
37 Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
38