Skip to content

Commit 6f4d988

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 re-render dispatcher, too.
1 parent 33394cb commit 6f4d988

File tree

2 files changed

+60
-6
lines changed

2 files changed

+60
-6
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,26 @@ function updateDeferredValue<T>(
11981198
return prevValue;
11991199
}
12001200

1201+
function rerenderDeferredValue<T>(
1202+
value: T,
1203+
config: TimeoutConfig | void | null,
1204+
): T {
1205+
const [prevValue, setValue] = rerenderState(value);
1206+
updateEffect(
1207+
() => {
1208+
const previousConfig = ReactCurrentBatchConfig.suspense;
1209+
ReactCurrentBatchConfig.suspense = config === undefined ? null : config;
1210+
try {
1211+
setValue(value);
1212+
} finally {
1213+
ReactCurrentBatchConfig.suspense = previousConfig;
1214+
}
1215+
},
1216+
[value, config],
1217+
);
1218+
return prevValue;
1219+
}
1220+
12011221
function startTransition(setPending, config, callback) {
12021222
const priorityLevel = getCurrentPriorityLevel();
12031223
runWithPriority(
@@ -1243,6 +1263,17 @@ function updateTransition(
12431263
return [start, isPending];
12441264
}
12451265

1266+
function rerenderTransition(
1267+
config: SuspenseConfig | void | null,
1268+
): [(() => void) => void, boolean] {
1269+
const [isPending, setPending] = rerenderState(false);
1270+
const start = updateCallback(startTransition.bind(null, setPending, config), [
1271+
setPending,
1272+
config,
1273+
]);
1274+
return [start, isPending];
1275+
}
1276+
12461277
function dispatchAction<S, A>(
12471278
fiber: Fiber,
12481279
queue: UpdateQueue<S, A>,
@@ -1420,8 +1451,8 @@ const HooksDispatcherOnRerender: Dispatcher = {
14201451
useState: rerenderState,
14211452
useDebugValue: updateDebugValue,
14221453
useResponder: createDeprecatedResponderListener,
1423-
useDeferredValue: updateDeferredValue,
1424-
useTransition: updateTransition,
1454+
useDeferredValue: rerenderDeferredValue,
1455+
useTransition: rerenderTransition,
14251456
};
14261457

14271458
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
@@ -1913,14 +1944,14 @@ if (__DEV__) {
19131944
useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T {
19141945
currentHookNameInDev = 'useDeferredValue';
19151946
updateHookTypesDev();
1916-
return updateDeferredValue(value, config);
1947+
return rerenderDeferredValue(value, config);
19171948
},
19181949
useTransition(
19191950
config: SuspenseConfig | void | null,
19201951
): [(() => void) => void, boolean] {
19211952
currentHookNameInDev = 'useTransition';
19221953
updateHookTypesDev();
1923-
return updateTransition(config);
1954+
return rerenderTransition(config);
19241955
},
19251956
};
19261957

@@ -2305,15 +2336,15 @@ if (__DEV__) {
23052336
currentHookNameInDev = 'useDeferredValue';
23062337
warnInvalidHookAccess();
23072338
updateHookTypesDev();
2308-
return updateDeferredValue(value, config);
2339+
return rerenderDeferredValue(value, config);
23092340
},
23102341
useTransition(
23112342
config: SuspenseConfig | void | null,
23122343
): [(() => void) => void, boolean] {
23132344
currentHookNameInDev = 'useTransition';
23142345
warnInvalidHookAccess();
23152346
updateHookTypesDev();
2316-
return updateTransition(config);
2347+
return rerenderTransition(config);
23172348
},
23182349
};
23192350
}

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

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

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

0 commit comments

Comments
 (0)