Remove renderPhaseUpdates Map (#17625)
* Remove renderPhaseUpdates Map Follow up to #17484, which was reverted due to a bug found in www. * Failing test: Dropped updates When resetting render phase updates after a throw, we should only clear the pending queue of hooks that were already processed. * Fix non-render-phase updates being dropped Detects if a queue has been processed by whether the hook was cloned. If we change the implementation to an array instead of a list, we'll need some other mechanism to determine whether the hook was processed. * 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.
Andrew Clark committed
Jan 17, 2020 at 16:00 UTC
95bd7aad7daa80c381faa3215c80b0906ab5ead5
4 files changed
+563
-147
packages/react-reconciler/src/ReactFiberBeginWork.js
+3
-2
@@ -137,7 +137,7 @@ import {
137
calculateChangedBits,
138
scheduleWorkOnParentPath,
139
} from './ReactFiberNewContext';
140
-import {resetHooks, renderWithHooks, bailoutHooks} from './ReactFiberHooks';
140
+import {renderWithHooks, bailoutHooks} from './ReactFiberHooks';
141
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer';
142
import {
143
getMaskedContext,
@@ -1407,7 +1407,8 @@ function mountIndeterminateComponent(
1407
workInProgress.tag = ClassComponent;
1408
1409
// Throw out any hooks that were used.
1410
- resetHooks();
1410
+ workInProgress.memoizedState = null;
1411
+ workInProgress.updateQueue = null;
1412
1413
// Push context providers early to prevent context stack mismatches.
1414
// During mounting we don't know the child context yet as the instance doesn't exist.
packages/react-reconciler/src/ReactFiberHooks.js
+431
-142
@@ -177,23 +177,12 @@ let currentlyRenderingFiber: Fiber = (null: any);
177
let currentHook: Hook | null = null;
178
let workInProgressHook: Hook | null = null;
179
180
-// Updates scheduled during render will trigger an immediate re-render at the
181
-// end of the current pass. We can't store these updates on the normal queue,
182
-// because if the work is aborted, they should be discarded. Because this is
183
-// a relatively rare case, we also don't want to add an additional field to
184
-// either the hook or queue object types. So we store them in a lazily create
185
-// map of queue -> render-phase updates, which are discarded once the component
186
-// completes without re-rendering.
187
-
188
-// Whether an update was scheduled during the currently executing render pass.
180
+// Whether an update was scheduled at any point during the render phase. This
181
+// does not get reset if we do another render pass; only when we're completely
182
+// finished evaluating this component. This is an optimization so we know
183
+// whether we need to clear render phase updates after a throw.
184
let didScheduleRenderPhaseUpdate: boolean = false;
190
-// Lazily created map of render-phase updates
191
-let renderPhaseUpdates: Map<
192
- UpdateQueue<any, any>,
193
- Update<any, any>,
194
-> | null = null;
195
-// Counter to prevent infinite loops.
196
-let numberOfReRenders: number = 0;
185
+
186
const RE_RENDER_LIMIT = 25;
187
188
// In DEV, this is the name of the currently executing primitive hook
@@ -387,8 +376,6 @@ export function renderWithHooks(
376
// workInProgressHook = null;
377
378
// didScheduleRenderPhaseUpdate = false;
390
- // renderPhaseUpdates = null;
391
- // numberOfReRenders = 0;
379
380
// TODO Warn if no hooks are used at all during mount, then some are used during update.
381
// Currently we will identify the update render as a mount because memoizedState === null.
@@ -419,9 +406,20 @@ export function renderWithHooks(
406
407
let children = Component(props, secondArg);
408
422
- if (didScheduleRenderPhaseUpdate) {
409
+ // Check if there was a render phase update
410
+ if (workInProgress.expirationTime === renderExpirationTime) {
411
+ // Keep rendering in a loop for as long as render phase updates continue to
412
+ // be scheduled. Use a counter to prevent infinite loops.
413
+ let numberOfReRenders: number = 0;
414
do {
424
- didScheduleRenderPhaseUpdate = false;
415
+ workInProgress.expirationTime = NoWork;
416
+
417
+ invariant(
418
+ numberOfReRenders < RE_RENDER_LIMIT,
419
+ 'Too many re-renders. React limits the number of renders to prevent ' +
420
+ 'an infinite loop.',
421
+ );
422
+
423
numberOfReRenders += 1;
424
if (__DEV__) {
425
// Even when hot reloading, allow dependencies to stabilize
@@ -441,14 +439,11 @@ export function renderWithHooks(
439
}
440
441
ReactCurrentDispatcher.current = __DEV__
444
- ? HooksDispatcherOnUpdateInDEV
445
- : HooksDispatcherOnUpdate;
442
+ ? HooksDispatcherOnRerenderInDEV
443
+ : HooksDispatcherOnRerender;
444
445
children = Component(props, secondArg);
448
- } while (didScheduleRenderPhaseUpdate);
449
-
450
- renderPhaseUpdates = null;
451
- numberOfReRenders = 0;
446
+ } while (workInProgress.expirationTime === renderExpirationTime);
447
}
448
449
// We can assume the previous dispatcher is always this one, since we set it
@@ -476,10 +471,7 @@ export function renderWithHooks(
471
hookTypesUpdateIndexDev = -1;
472
}
473
479
- // These were reset above
480
- // didScheduleRenderPhaseUpdate = false;
481
- // renderPhaseUpdates = null;
482
- // numberOfReRenders = 0;
474
+ didScheduleRenderPhaseUpdate = false;
475
476
invariant(
477
!didRenderTooFewHooks,
@@ -502,14 +494,29 @@ export function bailoutHooks(
494
}
495
}
496
505
-export function resetHooks(): void {
497
+export function resetHooksAfterThrow(): void {
498
// We can assume the previous dispatcher is always this one, since we set it
499
// at the beginning of the render phase and there's no re-entrancy.
500
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
501
510
- // This is used to reset the state of this module when a component throws.
511
- // It's also called inside mountIndeterminateComponent if we determine the
512
- // component is a module-style component.
502
+ if (didScheduleRenderPhaseUpdate) {
503
+ // There were render phase updates. These are only valid for this render
504
+ // phase, which we are now aborting. Remove the updates from the queues so
505
+ // they do not persist to the next render. Do not remove updates from hooks
506
+ // that weren't processed.
507
+ //
508
+ // Only reset the updates from the queue if it has a clone. If it does
509
+ // not have a clone, that means it wasn't processed, and the updates were
510
+ // scheduled before we entered the render phase.
511
+ let hook: Hook | null = currentlyRenderingFiber.memoizedState;
512
+ while (hook !== null) {
513
+ const queue = hook.queue;
514
+ if (queue !== null) {
515
+ queue.pending = null;
516
+ }
517
+ hook = hook.next;
518
+ }
519
+ }
520
521
renderExpirationTime = NoWork;
522
currentlyRenderingFiber = (null: any);
@@ -525,8 +532,6 @@ export function resetHooks(): void {
532
}
533
534
didScheduleRenderPhaseUpdate = false;
528
- renderPhaseUpdates = null;
529
- numberOfReRenders = 0;
535
}
536
537
function mountWorkInProgressHook(): Hook {
@@ -662,49 +667,6 @@ function updateReducer<S, I, A>(
667
668
queue.lastRenderedReducer = reducer;
669
665
- if (numberOfReRenders > 0) {
666
- // This is a re-render. Apply the new render phase updates to the previous
667
- // work-in-progress hook.
668
- const dispatch: Dispatch<A> = (queue.dispatch: any);
669
- if (renderPhaseUpdates !== null) {
670
- // Render phase updates are stored in a map of queue -> linked list
671
- const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
672
- if (firstRenderPhaseUpdate !== undefined) {
673
- renderPhaseUpdates.delete(queue);
674
- let newState = hook.memoizedState;
675
- let update = firstRenderPhaseUpdate;
676
- do {
677
- // Process this render phase update. We don't have to check the
678
- // priority because it will always be the same as the current
679
- // render's.
680
- const action = update.action;
681
- newState = reducer(newState, action);
682
- update = update.next;
683
- } while (update !== null);
684
-
685
- // Mark that the fiber performed work, but only if the new state is
686
- // different from the current state.
687
- if (!is(newState, hook.memoizedState)) {
688
- markWorkInProgressReceivedUpdate();
689
- }
690
-
691
- hook.memoizedState = newState;
692
- // Don't persist the state accumulated from the render phase updates to
693
- // the base state unless the queue is empty.
694
- // TODO: Not sure if this is the desired semantics, but it's what we
695
- // do for gDSFP. I can't remember why.
696
- if (hook.baseQueue === null) {
697
- hook.baseState = newState;
698
- }
699
-
700
- queue.lastRenderedState = newState;
701
-
702
- return [newState, dispatch];
703
- }
704
- }
705
- return [hook.memoizedState, dispatch];
706
- }
707
-
670
const current: Hook = (currentHook: any);
671
672
// The last rebase update that is NOT part of the base state.
@@ -822,6 +784,60 @@ function updateReducer<S, I, A>(
784
return [hook.memoizedState, dispatch];
785
}
786
787
+function rerenderReducer<S, I, A>(
788
+ reducer: (S, A) => S,
789
+ initialArg: I,
790
+ init?: I => S,
791
+): [S, Dispatch<A>] {
792
+ const hook = updateWorkInProgressHook();
793
+ const queue = hook.queue;
794
+ invariant(
795
+ queue !== null,
796
+ 'Should have a queue. This is likely a bug in React. Please file an issue.',
797
+ );
798
+
799
+ queue.lastRenderedReducer = reducer;
800
+
801
+ // This is a re-render. Apply the new render phase updates to the previous
802
+ // work-in-progress hook.
803
+ const dispatch: Dispatch<A> = (queue.dispatch: any);
804
+ const lastRenderPhaseUpdate = queue.pending;
805
+ let newState = hook.memoizedState;
806
+ if (lastRenderPhaseUpdate !== null) {
807
+ // The queue doesn't persist past this render pass.
808
+ queue.pending = null;
809
+
810
+ const firstRenderPhaseUpdate = lastRenderPhaseUpdate.next;
811
+ let update = firstRenderPhaseUpdate;
812
+ do {
813
+ // Process this render phase update. We don't have to check the
814
+ // priority because it will always be the same as the current
815
+ // render's.
816
+ const action = update.action;
817
+ newState = reducer(newState, action);
818
+ update = update.next;
819
+ } while (update !== firstRenderPhaseUpdate);
820
+
821
+ // Mark that the fiber performed work, but only if the new state is
822
+ // different from the current state.
823
+ if (!is(newState, hook.memoizedState)) {
824
+ markWorkInProgressReceivedUpdate();
825
+ }
826
+
827
+ hook.memoizedState = newState;
828
+ // Don't persist the state accumulated from the render phase updates to
829
+ // the base state unless the queue is empty.
830
+ // TODO: Not sure if this is the desired semantics, but it's what we
831
+ // do for gDSFP. I can't remember why.
832
+ if (hook.baseQueue === null) {
833
+ hook.baseState = newState;
834
+ }
835
+
836
+ queue.lastRenderedState = newState;
837
+ }
838
+ return [newState, dispatch];
839
+}
840
+
841
function mountState<S>(
842
initialState: (() => S) | S,
843
): [S, Dispatch<BasicStateAction<S>>] {
@@ -852,6 +868,12 @@ function updateState<S>(
868
return updateReducer(basicStateReducer, (initialState: any));
869
}
870
871
+function rerenderState<S>(
872
+ initialState: (() => S) | S,
873
+): [S, Dispatch<BasicStateAction<S>>] {
874
+ return rerenderReducer(basicStateReducer, (initialState: any));
875
+}
876
+
877
function pushEffect(tag, create, destroy, deps) {
878
const effect: Effect = {
879
tag,
@@ -1165,6 +1187,23 @@ function updateDeferredValue<T>(
1187
return prevValue;
1188
}
1189
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
+
1207
function startTransition(setPending, config, callback) {
1208
const priorityLevel = getCurrentPriorityLevel();
1209
runWithPriority(
@@ -1210,17 +1249,22 @@ function updateTransition(
1249
return [start, isPending];
1250
}
1251
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
+
1263
function dispatchAction<S, A>(
1264
fiber: Fiber,
1265
queue: UpdateQueue<S, A>,
1266
action: A,
1267
) {
1218
- invariant(
1219
- numberOfReRenders < RE_RENDER_LIMIT,
1220
- 'Too many re-renders. React limits the number of renders to prevent ' +
1221
- 'an infinite loop.',
1222
- );
1223
-
1268
if (__DEV__) {
1269
if (typeof arguments[3] === 'function') {
1270
console.error(
@@ -1231,6 +1275,38 @@ function dispatchAction<S, A>(
1275
}
1276
}
1277
1278
+ const currentTime = requestCurrentTimeForUpdate();
1279
+ const suspenseConfig = requestCurrentSuspenseConfig();
1280
+ const expirationTime = computeExpirationForFiber(
1281
+ currentTime,
1282
+ fiber,
1283
+ suspenseConfig,
1284
+ );
1285
+
1286
+ const update: Update<S, A> = {
1287
+ expirationTime,
1288
+ suspenseConfig,
1289
+ action,
1290
+ eagerReducer: null,
1291
+ eagerState: null,
1292
+ next: (null: any),
1293
+ };
1294
+
1295
+ if (__DEV__) {
1296
+ update.priority = getCurrentPriorityLevel();
1297
+ }
1298
+
1299
+ // Append the update to the end of the list.
1300
+ const pending = queue.pending;
1301
+ if (pending === null) {
1302
+ // This is the first update. Create a circular list.
1303
+ update.next = update;
1304
+ } else {
1305
+ update.next = pending.next;
1306
+ pending.next = update;
1307
+ }
1308
+ queue.pending = update;
1309
+
1310
const alternate = fiber.alternate;
1311
if (
1312
fiber === currentlyRenderingFiber ||
@@ -1240,64 +1316,9 @@ function dispatchAction<S, A>(
1316
// queue -> linked list of updates. After this render pass, we'll restart
1317
// and apply the stashed updates on top of the work-in-progress hook.
1318
didScheduleRenderPhaseUpdate = true;
1243
- const update: Update<S, A> = {
1244
- expirationTime: renderExpirationTime,
1245
- suspenseConfig: null,
1246
- action,
1247
- eagerReducer: null,
1248
- eagerState: null,
1249
- next: (null: any),
1250
- };
1251
- if (__DEV__) {
1252
- update.priority = getCurrentPriorityLevel();
1253
- }
1254
- if (renderPhaseUpdates === null) {
1255
- renderPhaseUpdates = new Map();
1256
- }
1257
- const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
1258
- if (firstRenderPhaseUpdate === undefined) {
1259
- renderPhaseUpdates.set(queue, update);
1260
- } else {
1261
- // Append the update to the end of the list.
1262
- let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
1263
- while (lastRenderPhaseUpdate.next !== null) {
1264
- lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
1265
- }
1266
- lastRenderPhaseUpdate.next = update;
1267
- }
1319
+ update.expirationTime = renderExpirationTime;
1320
+ currentlyRenderingFiber.expirationTime = renderExpirationTime;
1321
} else {
1269
- const currentTime = requestCurrentTimeForUpdate();
1270
- const suspenseConfig = requestCurrentSuspenseConfig();
1271
- const expirationTime = computeExpirationForFiber(
1272
- currentTime,
1273
- fiber,
1274
- suspenseConfig,
1275
- );
1276
-
1277
- const update: Update<S, A> = {
1278
- expirationTime,
1279
- suspenseConfig,
1280
- action,
1281
- eagerReducer: null,
1282
- eagerState: null,
1283
- next: (null: any),
1284
- };
1285
-
1286
- if (__DEV__) {
1287
- update.priority = getCurrentPriorityLevel();
1288
- }
1289
-
1290
- // Append the update to the end of the list.
1291
- const pending = queue.pending;
1292
- if (pending === null) {
1293
- // This is the first update. Create a circular list.
1294
- update.next = update;
1295
- } else {
1296
- update.next = pending.next;
1297
- pending.next = update;
1298
- }
1299
- queue.pending = update;
1300
-
1322
if (
1323
fiber.expirationTime === NoWork &&
1324
(alternate === null || alternate.expirationTime === NoWork)
@@ -1402,11 +1423,31 @@ const HooksDispatcherOnUpdate: Dispatcher = {
1423
useTransition: updateTransition,
1424
};
1425
1426
+const HooksDispatcherOnRerender: Dispatcher = {
1427
+ readContext,
1428
+
1429
+ useCallback: updateCallback,
1430
+ useContext: readContext,
1431
+ useEffect: updateEffect,
1432
+ useImperativeHandle: updateImperativeHandle,
1433
+ useLayoutEffect: updateLayoutEffect,
1434
+ useMemo: updateMemo,
1435
+ useReducer: rerenderReducer,
1436
+ useRef: updateRef,
1437
+ useState: rerenderState,
1438
+ useDebugValue: updateDebugValue,
1439
+ useResponder: createDeprecatedResponderListener,
1440
+ useDeferredValue: rerenderDeferredValue,
1441
+ useTransition: rerenderTransition,
1442
+};
1443
+
1444
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
1445
let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
1446
let HooksDispatcherOnUpdateInDEV: Dispatcher | null = null;
1447
+let HooksDispatcherOnRerenderInDEV: Dispatcher | null = null;
1448
let InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher | null = null;
1449
let InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher | null = null;
1450
+let InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher | null = null;
1451
1452
if (__DEV__) {
1453
const warnInvalidContextAccess = () => {
@@ -1783,6 +1824,123 @@ if (__DEV__) {
1824
},
1825
};
1826
1827
+ HooksDispatcherOnRerenderInDEV = {
1828
+ readContext<T>(
1829
+ context: ReactContext<T>,
1830
+ observedBits: void | number | boolean,
1831
+ ): T {
1832
+ return readContext(context, observedBits);
1833
+ },
1834
+
1835
+ useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
1836
+ currentHookNameInDev = 'useCallback';
1837
+ updateHookTypesDev();
1838
+ return updateCallback(callback, deps);
1839
+ },
1840
+ useContext<T>(
1841
+ context: ReactContext<T>,
1842
+ observedBits: void | number | boolean,
1843
+ ): T {
1844
+ currentHookNameInDev = 'useContext';
1845
+ updateHookTypesDev();
1846
+ return readContext(context, observedBits);
1847
+ },
1848
+ useEffect(
1849
+ create: () => (() => void) | void,
1850
+ deps: Array<mixed> | void | null,
1851
+ ): void {
1852
+ currentHookNameInDev = 'useEffect';
1853
+ updateHookTypesDev();
1854
+ return updateEffect(create, deps);
1855
+ },
1856
+ useImperativeHandle<T>(
1857
+ ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
1858
+ create: () => T,
1859
+ deps: Array<mixed> | void | null,
1860
+ ): void {
1861
+ currentHookNameInDev = 'useImperativeHandle';
1862
+ updateHookTypesDev();
1863
+ return updateImperativeHandle(ref, create, deps);
1864
+ },
1865
+ useLayoutEffect(
1866
+ create: () => (() => void) | void,
1867
+ deps: Array<mixed> | void | null,
1868
+ ): void {
1869
+ currentHookNameInDev = 'useLayoutEffect';
1870
+ updateHookTypesDev();
1871
+ return updateLayoutEffect(create, deps);
1872
+ },
1873
+ useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
1874
+ currentHookNameInDev = 'useMemo';
1875
+ updateHookTypesDev();
1876
+ const prevDispatcher = ReactCurrentDispatcher.current;
1877
+ ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnRerenderInDEV;
1878
+ try {
1879
+ return updateMemo(create, deps);
1880
+ } finally {
1881
+ ReactCurrentDispatcher.current = prevDispatcher;
1882
+ }
1883
+ },
1884
+ useReducer<S, I, A>(
1885
+ reducer: (S, A) => S,
1886
+ initialArg: I,
1887
+ init?: I => S,
1888
+ ): [S, Dispatch<A>] {
1889
+ currentHookNameInDev = 'useReducer';
1890
+ updateHookTypesDev();
1891
+ const prevDispatcher = ReactCurrentDispatcher.current;
1892
+ ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnRerenderInDEV;
1893
+ try {
1894
+ return rerenderReducer(reducer, initialArg, init);
1895
+ } finally {
1896
+ ReactCurrentDispatcher.current = prevDispatcher;
1897
+ }
1898
+ },
1899
+ useRef<T>(initialValue: T): {|current: T|} {
1900
+ currentHookNameInDev = 'useRef';
1901
+ updateHookTypesDev();
1902
+ return updateRef(initialValue);
1903
+ },
1904
+ useState<S>(
1905
+ initialState: (() => S) | S,
1906
+ ): [S, Dispatch<BasicStateAction<S>>] {
1907
+ currentHookNameInDev = 'useState';
1908
+ updateHookTypesDev();
1909
+ const prevDispatcher = ReactCurrentDispatcher.current;
1910
+ ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnRerenderInDEV;
1911
+ try {
1912
+ return rerenderState(initialState);
1913
+ } finally {
1914
+ ReactCurrentDispatcher.current = prevDispatcher;
1915
+ }
1916
+ },
1917
+ useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
1918
+ currentHookNameInDev = 'useDebugValue';
1919
+ updateHookTypesDev();
1920
+ return updateDebugValue(value, formatterFn);
1921
+ },
1922
+ useResponder<E, C>(
1923
+ responder: ReactEventResponder<E, C>,
1924
+ props,
1925
+ ): ReactEventResponderListener<E, C> {
1926
+ currentHookNameInDev = 'useResponder';
1927
+ updateHookTypesDev();
1928
+ return createDeprecatedResponderListener(responder, props);
1929
+ },
1930
+ useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T {
1931
+ currentHookNameInDev = 'useDeferredValue';
1932
+ updateHookTypesDev();
1933
+ return rerenderDeferredValue(value, config);
1934
+ },
1935
+ useTransition(
1936
+ config: SuspenseConfig | void | null,
1937
+ ): [(() => void) => void, boolean] {
1938
+ currentHookNameInDev = 'useTransition';
1939
+ updateHookTypesDev();
1940
+ return rerenderTransition(config);
1941
+ },
1942
+ };
1943
+
1944
InvalidNestedHooksDispatcherOnMountInDEV = {
1945
readContext<T>(
1946
context: ReactContext<T>,
@@ -2044,4 +2202,135 @@ if (__DEV__) {
2202
return updateTransition(config);
2203
},
2204
};
2205
+
2206
+ InvalidNestedHooksDispatcherOnRerenderInDEV = {
2207
+ readContext<T>(
2208
+ context: ReactContext<T>,
2209
+ observedBits: void | number | boolean,
2210
+ ): T {
2211
+ warnInvalidContextAccess();
2212
+ return readContext(context, observedBits);
2213
+ },
2214
+
2215
+ useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
2216
+ currentHookNameInDev = 'useCallback';
2217
+ warnInvalidHookAccess();
2218
+ updateHookTypesDev();
2219
+ return updateCallback(callback, deps);
2220
+ },
2221
+ useContext<T>(
2222
+ context: ReactContext<T>,
2223
+ observedBits: void | number | boolean,
2224
+ ): T {
2225
+ currentHookNameInDev = 'useContext';
2226
+ warnInvalidHookAccess();
2227
+ updateHookTypesDev();
2228
+ return readContext(context, observedBits);
2229
+ },
2230
+ useEffect(
2231
+ create: () => (() => void) | void,
2232
+ deps: Array<mixed> | void | null,
2233
+ ): void {
2234
+ currentHookNameInDev = 'useEffect';
2235
+ warnInvalidHookAccess();
2236
+ updateHookTypesDev();
2237
+ return updateEffect(create, deps);
2238
+ },
2239
+ useImperativeHandle<T>(
2240
+ ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
2241
+ create: () => T,
2242
+ deps: Array<mixed> | void | null,
2243
+ ): void {
2244
+ currentHookNameInDev = 'useImperativeHandle';
2245
+ warnInvalidHookAccess();
2246
+ updateHookTypesDev();
2247
+ return updateImperativeHandle(ref, create, deps);
2248
+ },
2249
+ useLayoutEffect(
2250
+ create: () => (() => void) | void,
2251
+ deps: Array<mixed> | void | null,
2252
+ ): void {
2253
+ currentHookNameInDev = 'useLayoutEffect';
2254
+ warnInvalidHookAccess();
2255
+ updateHookTypesDev();
2256
+ return updateLayoutEffect(create, deps);
2257
+ },
2258
+ useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
2259
+ currentHookNameInDev = 'useMemo';
2260
+ warnInvalidHookAccess();
2261
+ updateHookTypesDev();
2262
+ const prevDispatcher = ReactCurrentDispatcher.current;
2263
+ ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
2264
+ try {
2265
+ return updateMemo(create, deps);
2266
+ } finally {
2267
+ ReactCurrentDispatcher.current = prevDispatcher;
2268
+ }
2269
+ },
2270
+ useReducer<S, I, A>(
2271
+ reducer: (S, A) => S,
2272
+ initialArg: I,
2273
+ init?: I => S,
2274
+ ): [S, Dispatch<A>] {
2275
+ currentHookNameInDev = 'useReducer';
2276
+ warnInvalidHookAccess();
2277
+ updateHookTypesDev();
2278
+ const prevDispatcher = ReactCurrentDispatcher.current;
2279
+ ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
2280
+ try {
2281
+ return rerenderReducer(reducer, initialArg, init);
2282
+ } finally {
2283
+ ReactCurrentDispatcher.current = prevDispatcher;
2284
+ }
2285
+ },
2286
+ useRef<T>(initialValue: T): {|current: T|} {
2287
+ currentHookNameInDev = 'useRef';
2288
+ warnInvalidHookAccess();
2289
+ updateHookTypesDev();
2290
+ return updateRef(initialValue);
2291
+ },
2292
+ useState<S>(
2293
+ initialState: (() => S) | S,
2294
+ ): [S, Dispatch<BasicStateAction<S>>] {
2295
+ currentHookNameInDev = 'useState';
2296
+ warnInvalidHookAccess();
2297
+ updateHookTypesDev();
2298
+ const prevDispatcher = ReactCurrentDispatcher.current;
2299
+ ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
2300
+ try {
2301
+ return rerenderState(initialState);
2302
+ } finally {
2303
+ ReactCurrentDispatcher.current = prevDispatcher;
2304
+ }
2305
+ },
2306
+ useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
2307
+ currentHookNameInDev = 'useDebugValue';
2308
+ warnInvalidHookAccess();
2309
+ updateHookTypesDev();
2310
+ return updateDebugValue(value, formatterFn);
2311
+ },
2312
+ useResponder<E, C>(
2313
+ responder: ReactEventResponder<E, C>,
2314
+ props,
2315
+ ): ReactEventResponderListener<E, C> {
2316
+ currentHookNameInDev = 'useResponder';
2317
+ warnInvalidHookAccess();
2318
+ updateHookTypesDev();
2319
+ return createDeprecatedResponderListener(responder, props);
2320
+ },
2321
+ useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T {
2322
+ currentHookNameInDev = 'useDeferredValue';
2323
+ warnInvalidHookAccess();
2324
+ updateHookTypesDev();
2325
+ return rerenderDeferredValue(value, config);
2326
+ },
2327
+ useTransition(
2328
+ config: SuspenseConfig | void | null,
2329
+ ): [(() => void) => void, boolean] {
2330
+ currentHookNameInDev = 'useTransition';
2331
+ warnInvalidHookAccess();
2332
+ updateHookTypesDev();
2333
+ return rerenderTransition(config);
2334
+ },
2335
+ };
2336
}
packages/react-reconciler/src/ReactFiberWorkLoop.js
+3
-3
@@ -141,7 +141,7 @@ import {
141
} from './ReactFiberCommitWork';
142
import {enqueueUpdate} from './ReactUpdateQueue';
143
import {resetContextDependencies} from './ReactFiberNewContext';
144
-import {resetHooks, ContextOnlyDispatcher} from './ReactFiberHooks';
144
+import {resetHooksAfterThrow, ContextOnlyDispatcher} from './ReactFiberHooks';
145
import {createCapturedValue} from './ReactCapturedValue';
146
147
import {
@@ -1281,7 +1281,7 @@ function handleError(root, thrownValue) {
1281
try {
1282
// Reset module-level state that was set during the render phase.
1283
resetContextDependencies();
1284
- resetHooks();
1284
+ resetHooksAfterThrow();
1285
resetCurrentDebugFiberInDEV();
1286
1287
if (workInProgress === null || workInProgress.return === null) {
@@ -2636,7 +2636,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
2636
// Keep this code in sync with handleError; any changes here must have
2637
// corresponding changes there.
2638
resetContextDependencies();
2639
- resetHooks();
2639
+ resetHooksAfterThrow();
2640
// Don't reset current debug fiber, since we're about to work on the
2641
// same fiber again.
2642
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+126
@@ -545,6 +545,132 @@ describe('ReactHooksWithNoopRenderer', () => {
545
]);
546
expect(ReactNoop.getChildren()).toEqual([span(22)]);
547
});
548
+
549
+ it('discards render phase updates if something suspends', () => {
550
+ const thenable = {then() {}};
551
+ function Foo({signal}) {
552
+ return (
553
+ <Suspense fallback="Loading...">
554
+ <Bar signal={signal} />
555
+ </Suspense>
556
+ );
557
+ }
558
+
559
+ function Bar({signal: newSignal}) {
560
+ let [counter, setCounter] = useState(0);
561
+ let [signal, setSignal] = useState(true);
562
+
563
+ // Increment a counter every time the signal changes
564
+ if (signal !== newSignal) {
565
+ setCounter(c => c + 1);
566
+ setSignal(newSignal);
567
+ if (counter === 0) {
568
+ // We're suspending during a render that includes render phase
569
+ // updates. Those updates should not persist to the next render.
570
+ Scheduler.unstable_yieldValue('Suspend!');
571
+ throw thenable;
572
+ }
573
+ }
574
+
575
+ return <Text text={counter} />;
576
+ }
577
+
578
+ const root = ReactNoop.createRoot();
579
+ root.render(<Foo signal={true} />);
580
+
581
+ expect(Scheduler).toFlushAndYield([0]);
582
+ expect(root).toMatchRenderedOutput(<span prop={0} />);
583
+
584
+ root.render(<Foo signal={false} />);
585
+ expect(Scheduler).toFlushAndYield(['Suspend!']);
586
+ expect(root).toMatchRenderedOutput(<span prop={0} />);
587
+
588
+ // Rendering again should suspend again.
589
+ root.render(<Foo signal={false} />);
590
+ expect(Scheduler).toFlushAndYield(['Suspend!']);
591
+ });
592
+
593
+ it('discards render phase updates if something suspends, but not other updates in the same component', async () => {
594
+ const thenable = {then() {}};
595
+ function Foo({signal}) {
596
+ return (
597
+ <Suspense fallback="Loading...">
598
+ <Bar signal={signal} />
599
+ </Suspense>
600
+ );
601
+ }
602
+
603
+ let setLabel;
604
+ function Bar({signal: newSignal}) {
605
+ let [counter, setCounter] = useState(0);
606
+
607
+ if (counter === 1) {
608
+ // We're suspending during a render that includes render phase
609
+ // updates. Those updates should not persist to the next render.
610
+ Scheduler.unstable_yieldValue('Suspend!');
611
+ throw thenable;
612
+ }
613
+
614
+ let [signal, setSignal] = useState(true);
615
+
616
+ // Increment a counter every time the signal changes
617
+ if (signal !== newSignal) {
618
+ setCounter(c => c + 1);
619
+ setSignal(newSignal);
620
+ }
621
+
622
+ let [label, _setLabel] = useState('A');
623
+ setLabel = _setLabel;
624
+
625
+ return <Text text={`${label}:${counter}`} />;
626
+ }
627
+
628
+ const root = ReactNoop.createRoot();
629
+ root.render(<Foo signal={true} />);
630
+
631
+ expect(Scheduler).toFlushAndYield(['A:0']);
632
+ expect(root).toMatchRenderedOutput(<span prop="A:0" />);
633
+
634
+ await ReactNoop.act(async () => {
635
+ root.render(<Foo signal={false} />);
636
+ setLabel('B');
637
+ });
638
+ expect(Scheduler).toHaveYielded(['Suspend!']);
639
+ expect(root).toMatchRenderedOutput(<span prop="A:0" />);
640
+
641
+ // Rendering again should suspend again.
642
+ root.render(<Foo signal={false} />);
643
+ expect(Scheduler).toFlushAndYield(['Suspend!']);
644
+
645
+ // Flip the signal back to "cancel" the update. However, the update to
646
+ // label should still proceed. It shouldn't have been dropped.
647
+ root.render(<Foo signal={true} />);
648
+ expect(Scheduler).toFlushAndYield(['B:0']);
649
+ expect(root).toMatchRenderedOutput(<span prop="B:0" />);
650
+ });
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
+ });
674
});
675
676
describe('useReducer', () => {