[compiler] Consider setter from useOptimistic non-reactive (#35141)
Closes https://github.com/facebook/react/issues/35138
Ricky committed
Nov 18, 2025 at 10:50 UTC
0972e23908b573faa5beebc0eb1154f792c53b62
5 files changed
+176
-2
compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts
+13
@@ -23,6 +23,7 @@ import {
23
BuiltInUseInsertionEffectHookId,
24
BuiltInUseLayoutEffectHookId,
25
BuiltInUseOperatorId,
26
+ BuiltInUseOptimisticId,
27
BuiltInUseReducerId,
28
BuiltInUseRefId,
29
BuiltInUseStateId,
@@ -818,6 +819,18 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
819
returnValueKind: ValueKind.Frozen,
820
}),
821
],
822
+ [
823
+ 'useOptimistic',
824
+ addHook(DEFAULT_SHAPES, {
825
+ positionalParams: [],
826
+ restParam: Effect.Freeze,
827
+ returnType: {kind: 'Object', shapeId: BuiltInUseOptimisticId},
828
+ calleeEffect: Effect.Read,
829
+ hookKind: 'useOptimistic',
830
+ returnValueKind: ValueKind.Frozen,
831
+ returnValueReason: ValueReason.State,
832
+ }),
833
+ ],
834
[
835
'use',
836
addFunction(
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+17
-2
@@ -1887,6 +1887,18 @@ export function isStartTransitionType(id: Identifier): boolean {
1887
);
1888
}
1889
1890
+export function isUseOptimisticType(id: Identifier): boolean {
1891
+ return (
1892
+ id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseOptimistic'
1893
+ );
1894
+}
1895
+
1896
+export function isSetOptimisticType(id: Identifier): boolean {
1897
+ return (
1898
+ id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetOptimistic'
1899
+ );
1900
+}
1901
+
1902
export function isSetActionStateType(id: Identifier): boolean {
1903
return (
1904
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetActionState'
@@ -1920,7 +1932,8 @@ export function isStableType(id: Identifier): boolean {
1932
isSetActionStateType(id) ||
1933
isDispatcherType(id) ||
1934
isUseRefType(id) ||
1923
- isStartTransitionType(id)
1935
+ isStartTransitionType(id) ||
1936
+ isSetOptimisticType(id)
1937
);
1938
}
1939
@@ -1931,8 +1944,9 @@ export function isStableTypeContainer(id: Identifier): boolean {
1944
}
1945
return (
1946
isUseStateType(id) || // setState
1934
- type_.shapeId === 'BuiltInUseActionState' || // setActionState
1947
+ isUseActionStateType(id) || // setActionState
1948
isUseReducerType(id) || // dispatcher
1949
+ isUseOptimisticType(id) || // setOptimistic
1950
type_.shapeId === 'BuiltInUseTransition' // startTransition
1951
);
1952
}
@@ -1952,6 +1966,7 @@ export function evaluatesToStableTypeOrContainer(
1966
case 'useActionState':
1967
case 'useRef':
1968
case 'useTransition':
1969
+ case 'useOptimistic':
1970
return true;
1971
}
1972
}
compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts
+22
@@ -304,6 +304,7 @@ export type HookKind =
304
| 'useTransition'
305
| 'useImperativeHandle'
306
| 'useEffectEvent'
307
+ | 'useOptimistic'
308
| 'Custom';
309
310
/*
@@ -399,6 +400,8 @@ export const BuiltInUseReducerId = 'BuiltInUseReducer';
400
export const BuiltInDispatchId = 'BuiltInDispatch';
401
export const BuiltInUseContextHookId = 'BuiltInUseContextHook';
402
export const BuiltInUseTransitionId = 'BuiltInUseTransition';
403
+export const BuiltInUseOptimisticId = 'BuiltInUseOptimistic';
404
+export const BuiltInSetOptimisticId = 'BuiltInSetOptimistic';
405
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
406
export const BuiltInFireId = 'BuiltInFire';
407
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
@@ -1186,6 +1189,25 @@ addObject(BUILTIN_SHAPES, BuiltInUseTransitionId, [
1189
],
1190
]);
1191
1192
+addObject(BUILTIN_SHAPES, BuiltInUseOptimisticId, [
1193
+ ['0', {kind: 'Poly'}],
1194
+ [
1195
+ '1',
1196
+ addFunction(
1197
+ BUILTIN_SHAPES,
1198
+ [],
1199
+ {
1200
+ positionalParams: [],
1201
+ restParam: Effect.Freeze,
1202
+ returnType: PRIMITIVE_TYPE,
1203
+ calleeEffect: Effect.Read,
1204
+ returnValueKind: ValueKind.Primitive,
1205
+ },
1206
+ BuiltInSetOptimisticId,
1207
+ ),
1208
+ ],
1209
+]);
1210
+
1211
addObject(BUILTIN_SHAPES, BuiltInUseActionStateId, [
1212
['0', {kind: 'Poly'}],
1213
[
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/preserve-use-callback-stable-built-ins.expect.md
new
+91
@@ -0,0 +1,91 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @validatePreserveExistingMemoizationGuarantees
6
+import {
7
+ useCallback,
8
+ useTransition,
9
+ useState,
10
+ useOptimistic,
11
+ useActionState,
12
+ useRef,
13
+ useReducer,
14
+} from 'react';
15
+
16
+function useFoo() {
17
+ const [s, setState] = useState();
18
+ const ref = useRef(null);
19
+ const [t, startTransition] = useTransition();
20
+ const [u, addOptimistic] = useOptimistic();
21
+ const [v, dispatch] = useReducer(() => {}, null);
22
+ const [isPending, dispatchAction] = useActionState(() => {}, null);
23
+
24
+ return useCallback(() => {
25
+ dispatch();
26
+ startTransition(() => {});
27
+ addOptimistic();
28
+ setState(null);
29
+ dispatchAction();
30
+ ref.current = true;
31
+ }, []);
32
+}
33
+
34
+export const FIXTURE_ENTRYPOINT = {
35
+ fn: useFoo,
36
+ params: [],
37
+};
38
+
39
+```
40
+
41
+## Code
42
+
43
+```javascript
44
+import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
45
+import {
46
+ useCallback,
47
+ useTransition,
48
+ useState,
49
+ useOptimistic,
50
+ useActionState,
51
+ useRef,
52
+ useReducer,
53
+} from "react";
54
+
55
+function useFoo() {
56
+ const $ = _c(1);
57
+ const [, setState] = useState();
58
+ const ref = useRef(null);
59
+ const [, startTransition] = useTransition();
60
+ const [, addOptimistic] = useOptimistic();
61
+ const [, dispatch] = useReducer(_temp, null);
62
+ const [, dispatchAction] = useActionState(_temp2, null);
63
+ let t0;
64
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
65
+ t0 = () => {
66
+ dispatch();
67
+ startTransition(_temp3);
68
+ addOptimistic();
69
+ setState(null);
70
+ dispatchAction();
71
+ ref.current = true;
72
+ };
73
+ $[0] = t0;
74
+ } else {
75
+ t0 = $[0];
76
+ }
77
+ return t0;
78
+}
79
+function _temp3() {}
80
+function _temp2() {}
81
+function _temp() {}
82
+
83
+export const FIXTURE_ENTRYPOINT = {
84
+ fn: useFoo,
85
+ params: [],
86
+};
87
+
88
+```
89
+
90
+### Eval output
91
+(kind: ok) "[[ function params=0 ]]"
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/preserve-use-callback-stable-built-ins.ts
new
+33
@@ -0,0 +1,33 @@
1
+// @validatePreserveExistingMemoizationGuarantees
2
+import {
3
+ useCallback,
4
+ useTransition,
5
+ useState,
6
+ useOptimistic,
7
+ useActionState,
8
+ useRef,
9
+ useReducer,
10
+} from 'react';
11
+
12
+function useFoo() {
13
+ const [s, setState] = useState();
14
+ const ref = useRef(null);
15
+ const [t, startTransition] = useTransition();
16
+ const [u, addOptimistic] = useOptimistic();
17
+ const [v, dispatch] = useReducer(() => {}, null);
18
+ const [isPending, dispatchAction] = useActionState(() => {}, null);
19
+
20
+ return useCallback(() => {
21
+ dispatch();
22
+ startTransition(() => {});
23
+ addOptimistic();
24
+ setState(null);
25
+ dispatchAction();
26
+ ref.current = true;
27
+ }, []);
28
+}
29
+
30
+export const FIXTURE_ENTRYPOINT = {
31
+ fn: useFoo,
32
+ params: [],
33
+};