Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,23 @@ class Visitor extends ReactiveFunctionVisitor<ReactiveIdentifiers> {
this.traverseScope(scopeBlock, state);
for (const dep of scopeBlock.scope.dependencies) {
const isReactive = state.has(dep.identifier.id);
if (!isReactive) {
/*
* Stable values (state setters, dispatch functions, etc.) are guaranteed
* to have a stable identity across renders and should never be scope
* dependencies. Even if they appear reactive due to how they're captured
* in closures, including them as deps produces unnecessary cache checks.
*/
/*
* Stable values (state setters, dispatch functions, etc.) are guaranteed
* to have a stable identity across renders and should never be scope
* dependencies. Even if they appear reactive due to how they're captured
* in closures, including them as deps produces unnecessary cache checks.
*
* Note: we guard on !dep.reactive to preserve reactive identifiers that
* happen to have a stable type (e.g. `const ref = cond ? ref1 : ref2`),
* which DO change identity based on reactive inputs.
*/
if (!isReactive || (!dep.reactive && isStableType(dep.identifier))) {
scopeBlock.scope.dependencies.delete(dep);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
ReactiveValue,
ScopeId,
SourceLocation,
isStableType,
} from '../HIR';
import {Environment} from '../HIR/Environment';
import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR';
Expand Down Expand Up @@ -233,6 +234,22 @@ function validateInferredDep(
env: Environment,
memoLocation: SourceLocation,
): void {
/*
* Stable values (state setters, dispatch functions, refs, etc.) that are
* not reactive are guaranteed to never change identity across renders. The
* Rules of React and exhaustive-deps rules do not require them in
* dependency arrays, so the preserve-manual-memoization validation should
* not require them either. Requiring stable, non-reactive values causes
* false-positive skips when user code correctly omits them from a manual
* `useCallback`/`useMemo` deps list.
*
* Note: we only skip non-reactive stable values. Reactive identifiers that
* happen to have a stable type (e.g. `const ref = cond ? ref1 : ref2`)
* still need to appear in the deps array because their identity changes.
*/
if (!dep.reactive && isStableType(dep.identifier)) {
return;
}
let normalizedDep: ManualMemoDependency;
const maybeNormalizedRoot = temporaries.get(dep.identifier.id);
if (maybeNormalizedRoot != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,42 +27,39 @@ function useFoo() {
```javascript
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
function useFoo() {
const $ = _c(7);
const $ = _c(4);
const onClick = (response) => {
setState(DISABLED_FORM);
};

const [, t0] = useState();
const setState = t0;
let t1;
if ($[0] !== setState) {
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
setState(DISABLED_FORM);
};
$[0] = setState;
$[1] = t1;
$[0] = t1;
} else {
t1 = $[1];
t1 = $[0];
}
setState;
const handleLogout = t1;
let t2;
if ($[2] !== handleLogout) {
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
const getComponent = () => <ColumnItem onPress={() => handleLogout()} />;
t2 = getComponent();
$[2] = handleLogout;
$[3] = t2;
$[1] = t2;
} else {
t2 = $[3];
t2 = $[1];
}
let t3;
if ($[4] !== onClick || $[5] !== t2) {
if ($[2] !== onClick) {
t3 = [t2, onClick];
$[4] = onClick;
$[5] = t2;
$[6] = t3;
$[2] = onClick;
$[3] = t3;
} else {
t3 = $[6];
t3 = $[3];
}
return t3;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@

## Input

```javascript
// @validatePreserveExistingMemoizationGuarantees
import {useCallback, useEffect, useState} from 'react';

/**
* Regression test for: https://github.com/facebook/react/issues/36384
*
* The compiler was incorrectly inferring `setImages` (a state setter) as a
* required dependency of the useCallback scope in this specific pattern,
* where JSON.parse of an external value is used as initial state.
*
* Without the fix, the compiler would produce:
* "Compilation Skipped: Existing memoization could not be preserved.
* The inferred dependency was `setImages`, but the source dependencies were []."
*
* This test should compile successfully — state setters are stable values
* and must not be required in manual dependency arrays.
*/

function getValue() {
return 'true';
}

function ImageLibraryPicker() {
const boolString = getValue();
const [images, setImages] = useState('');
const [booleanState, setBooleanState] = useState<boolean>(
JSON.parse(boolString),
);

const search = useCallback(() => {
setImages('');
}, []);

useEffect(() => {
search();
}, [search]);

return null;
}

export const FIXTURE_ENTRYPOINT = {
fn: ImageLibraryPicker,
params: [],
};

```

## Code

```javascript
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
import { useCallback, useEffect, useState } from "react";

/**
* Regression test for: https://github.com/facebook/react/issues/36384
*
* The compiler was incorrectly inferring `setImages` (a state setter) as a
* required dependency of the useCallback scope in this specific pattern,
* where JSON.parse of an external value is used as initial state.
*
* Without the fix, the compiler would produce:
* "Compilation Skipped: Existing memoization could not be preserved.
* The inferred dependency was `setImages`, but the source dependencies were []."
*
* This test should compile successfully — state setters are stable values
* and must not be required in manual dependency arrays.
*/

function getValue() {
return "true";
}

function ImageLibraryPicker() {
const $ = _c(3);
const boolString = getValue();
const [, setImages] = useState("");
useState(JSON.parse(boolString));
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => {
setImages("");
};
$[0] = t0;
} else {
t0 = $[0];
}
const search = t0;
let t1;
let t2;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
search();
};
t2 = [search];
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
useEffect(t1, t2);

return null;
}

export const FIXTURE_ENTRYPOINT = {
fn: ImageLibraryPicker,
params: [],
};

```

### Eval output
(kind: ok) null
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// @validatePreserveExistingMemoizationGuarantees
import {useCallback, useEffect, useState} from 'react';

/**
* Regression test for: https://github.com/facebook/react/issues/36384
*
* The compiler was incorrectly inferring `setImages` (a state setter) as a
* required dependency of the useCallback scope in this specific pattern,
* where JSON.parse of an external value is used as initial state.
*
* Without the fix, the compiler would produce:
* "Compilation Skipped: Existing memoization could not be preserved.
* The inferred dependency was `setImages`, but the source dependencies were []."
*
* This test should compile successfully — state setters are stable values
* and must not be required in manual dependency arrays.
*/

function getValue() {
return 'true';
}

function ImageLibraryPicker() {
const boolString = getValue();
const [images, setImages] = useState('');
const [booleanState, setBooleanState] = useState<boolean>(
JSON.parse(boolString),
);

const search = useCallback(() => {
setImages('');
}, []);

useEffect(() => {
search();
}, [search]);

return null;
}

export const FIXTURE_ENTRYPOINT = {
fn: ImageLibraryPicker,
params: [],
};
Loading
Loading