Skip to content

[compiler] fix: preserve-manual-memoization validation should not require stable (non-reactive) values in deps#36397

Open
sleitor wants to merge 1 commit intofacebook:mainfrom
sleitor:fix-36384
Open

[compiler] fix: preserve-manual-memoization validation should not require stable (non-reactive) values in deps#36397
sleitor wants to merge 1 commit intofacebook:mainfrom
sleitor:fix-36384

Conversation

@sleitor
Copy link
Copy Markdown
Contributor

@sleitor sleitor commented May 2, 2026

Summary

validatePreservedManualMemoization (run when validatePreserveExistingMemoizationGuarantees is enabled) was incorrectly requiring state setters, dispatch functions, and other stable values to appear in the manual dependency array of useCallback/useMemo calls.

The Rules of React explicitly state that stable values (state setters returned by useState, dispatch returned by useReducer, startTransition, refs, etc.) do not need to be listed in dependency arrays — they are guaranteed to never change identity across renders. The eslint-plugin-react-hooks/exhaustive-deps rule already implements this exemption.

Root cause

ValidatePreservedManualMemoization.validateInferredDep checked ALL inferred scope dependencies against the user-provided source deps list, with no exemption for stable values. When the compiler inferred that a state setter like setImages was a dependency of the memoized scope, it would produce:

Compilation Skipped: Existing memoization could not be preserved
The inferred dependency was `setImages`, but the source dependencies were [].

This caused the compiler to bail out on components that correctly omitted state setters from their useCallback / useMemo deps arrays.

Fix

Add an early return in validateInferredDep when !dep.reactive && isStableType(dep.identifier):

  • The !dep.reactive guard ensures we still validate reactive identifiers that happen to have a stable type — e.g. const ref = cond ? ref1 : ref2 (where the identity changes reactively).
  • isStableType covers SetState, SetActionState, dispatch, useRef, startTransition, and setOptimistic.

This mirrors the logic in ValidateExhaustiveDependencies.isOptionalDependency which already applies the same exemption.

How did you test this change?

  • Added new test fixture: preserve-memo-validation/useCallback-state-setter-stable-dep which exercises a component using setProcessed and setCount (state setters) inside useCallback with an empty deps array.
  • Verified the existing test preserve-memo-validation/error.preserve-use-memo-ref-missing-reactive (reactive conditional ref still required in deps) continues to fail as expected.
  • All 1720 compiler tests pass.

Fixes #36384

…uire stable (non-reactive) values in deps

State setters (from useState), dispatch functions (from useReducer),
startTransition, and other stable values guaranteed not to change
identity across renders should not be required in manual dependency
arrays. The Rules of React and the exhaustive-deps lint rule explicitly
exempt stable values from dependency lists.

However, ValidatePreservedManualMemoization's validateInferredDep was
checking ALL inferred dependencies against the user-provided source deps
list, including stable non-reactive values. This caused the compiler to
skip optimizing components that correctly omitted stable values like
state setters from their useCallback/useMemo dependency arrays.

Fix: skip the validateInferredDep check when dep.reactive is false AND
the identifier is a stable type (isStableType). We gate on !dep.reactive
to preserve the existing behavior for edge cases like
`const ref = cond ? ref1 : ref2` where a stable-typed identifier
happens to be reactive because its value depends on a reactive condition.

Fixes: facebook#36384
@meta-cla meta-cla Bot added the CLA Signed label May 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Compiler Bug]: Compiler requires state setter to be added to dependency array

1 participant