Skip to content

Commit 811a659

Browse files
committed
Regression test: startTransition in render phase
useTransition uses the state hook as part of its implementation, so we need to fork it in the dispatcher used for re-renders, too.
1 parent 22850fd commit 811a659

File tree

2 files changed

+57
-6
lines changed

2 files changed

+57
-6
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,23 @@ function updateDeferredValue<T>(
11871187
return prevValue;
11881188
}
11891189

1190+
function rerenderDeferredValue<T>(
1191+
value: T,
1192+
config: TimeoutConfig | void | null,
1193+
): T {
1194+
const [prevValue, setValue] = rerenderState(value);
1195+
updateEffect(() => {
1196+
const previousConfig = ReactCurrentBatchConfig.suspense;
1197+
ReactCurrentBatchConfig.suspense = config === undefined ? null : config;
1198+
try {
1199+
setValue(value);
1200+
} finally {
1201+
ReactCurrentBatchConfig.suspense = previousConfig;
1202+
}
1203+
}, [value, config]);
1204+
return prevValue;
1205+
}
1206+
11901207
function startTransition(setPending, config, callback) {
11911208
const priorityLevel = getCurrentPriorityLevel();
11921209
runWithPriority(
@@ -1232,6 +1249,17 @@ function updateTransition(
12321249
return [start, isPending];
12331250
}
12341251

1252+
function rerenderTransition(
1253+
config: SuspenseConfig | void | null,
1254+
): [(() => void) => void, boolean] {
1255+
const [isPending, setPending] = rerenderState(false);
1256+
const start = updateCallback(startTransition.bind(null, setPending, config), [
1257+
setPending,
1258+
config,
1259+
]);
1260+
return [start, isPending];
1261+
}
1262+
12351263
function dispatchAction<S, A>(
12361264
fiber: Fiber,
12371265
queue: UpdateQueue<S, A>,
@@ -1409,8 +1437,8 @@ const HooksDispatcherOnRerender: Dispatcher = {
14091437
useState: rerenderState,
14101438
useDebugValue: updateDebugValue,
14111439
useResponder: createDeprecatedResponderListener,
1412-
useDeferredValue: updateDeferredValue,
1413-
useTransition: updateTransition,
1440+
useDeferredValue: rerenderDeferredValue,
1441+
useTransition: rerenderTransition,
14141442
};
14151443

14161444
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
@@ -1902,14 +1930,14 @@ if (__DEV__) {
19021930
useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T {
19031931
currentHookNameInDev = 'useDeferredValue';
19041932
updateHookTypesDev();
1905-
return updateDeferredValue(value, config);
1933+
return rerenderDeferredValue(value, config);
19061934
},
19071935
useTransition(
19081936
config: SuspenseConfig | void | null,
19091937
): [(() => void) => void, boolean] {
19101938
currentHookNameInDev = 'useTransition';
19111939
updateHookTypesDev();
1912-
return updateTransition(config);
1940+
return rerenderTransition(config);
19131941
},
19141942
};
19151943

@@ -2294,15 +2322,15 @@ if (__DEV__) {
22942322
currentHookNameInDev = 'useDeferredValue';
22952323
warnInvalidHookAccess();
22962324
updateHookTypesDev();
2297-
return updateDeferredValue(value, config);
2325+
return rerenderDeferredValue(value, config);
22982326
},
22992327
useTransition(
23002328
config: SuspenseConfig | void | null,
23012329
): [(() => void) => void, boolean] {
23022330
currentHookNameInDev = 'useTransition';
23032331
warnInvalidHookAccess();
23042332
updateHookTypesDev();
2305-
return updateTransition(config);
2333+
return rerenderTransition(config);
23062334
},
23072335
};
23082336
}

packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,29 @@ describe('ReactHooksWithNoopRenderer', () => {
648648
expect(Scheduler).toFlushAndYield(['B:0']);
649649
expect(root).toMatchRenderedOutput(<span prop="B:0" />);
650650
});
651+
652+
// TODO: This should probably warn
653+
it.experimental('calling startTransition inside render phase', async () => {
654+
let startTransition;
655+
function App() {
656+
let [counter, setCounter] = useState(0);
657+
let [_startTransition] = useTransition();
658+
startTransition = _startTransition;
659+
660+
if (counter === 0) {
661+
startTransition(() => {
662+
setCounter(c => c + 1);
663+
});
664+
}
665+
666+
return <Text text={counter} />;
667+
}
668+
669+
const root = ReactNoop.createRoot();
670+
root.render(<App />);
671+
expect(Scheduler).toFlushAndYield([1]);
672+
expect(root).toMatchRenderedOutput(<span prop={1} />);
673+
});
651674
});
652675

653676
describe('useReducer', () => {

0 commit comments

Comments
 (0)