Remove renderPhaseUpdates Map (#17484)
* Render phase updates can now be extracted from the pending queue * Use a custom dispatcher for the second render pass * Discard render phase updates if component throws When aborting a render, we also need to throw out render phase updates. Remove the updates from the queues so they do not persist to the next render. We already did a single pass through the whole list of hooks, so we know that any pending updates must have been dispatched during the render phase. The ones that were dispatched before we started rendering were already transferred to the current hook's queue.
Sebastian Markbåge committed
Dec 13, 2019 at 16:45 UTC
8a347ed024159a5307172ea633a4561160f4a6b9
4 files changed
+444
-133
packages/react-reconciler/src/ReactFiberBeginWork.js
+3
-2
@@ -136,7 +136,7 @@ import {
136
calculateChangedBits,
137
scheduleWorkOnParentPath,
138
} from './ReactFiberNewContext';
139
-import {resetHooks, renderWithHooks, bailoutHooks} from './ReactFiberHooks';
139
+import {renderWithHooks, bailoutHooks} from './ReactFiberHooks';
140
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer';
141
import {
142
getMaskedContext,
@@ -1319,7 +1319,8 @@ function mountIndeterminateComponent(
1319
workInProgress.tag = ClassComponent;
1320
1321
// Throw out any hooks that were used.
1322
- resetHooks();
1322
+ workInProgress.memoizedState = null;
1323
+ workInProgress.updateQueue = null;
1324
1325
// Push context providers early to prevent context stack mismatches.
1326
// During mounting we don't know the child context yet as the instance doesn't exist.
packages/react-reconciler/src/ReactFiberHooks.js
+394
-128
@@ -193,13 +193,7 @@ let workInProgressHook: Hook | null = null;
193
194
// Whether an update was scheduled during the currently executing render pass.
195
let didScheduleRenderPhaseUpdate: boolean = false;
196
-// Lazily created map of render-phase updates
197
-let renderPhaseUpdates: Map<
198
- UpdateQueue<any, any>,
199
- Update<any, any>,
200
-> | null = null;
201
-// Counter to prevent infinite loops.
202
-let numberOfReRenders: number = 0;
196
+
197
const RE_RENDER_LIMIT = 25;
198
199
// In DEV, this is the name of the currently executing primitive hook
@@ -393,8 +387,6 @@ export function renderWithHooks(
387
// workInProgressHook = null;
388
389
// didScheduleRenderPhaseUpdate = false;
396
- // renderPhaseUpdates = null;
397
- // numberOfReRenders = 0;
390
391
// TODO Warn if no hooks are used at all during mount, then some are used during update.
392
// Currently we will identify the update render as a mount because memoizedState === null.
@@ -426,8 +418,17 @@ export function renderWithHooks(
418
let children = Component(props, refOrContext);
419
420
if (didScheduleRenderPhaseUpdate) {
421
+ // Counter to prevent infinite loops.
422
+ let numberOfReRenders: number = 0;
423
do {
424
didScheduleRenderPhaseUpdate = false;
425
+
426
+ invariant(
427
+ numberOfReRenders < RE_RENDER_LIMIT,
428
+ 'Too many re-renders. React limits the number of renders to prevent ' +
429
+ 'an infinite loop.',
430
+ );
431
+
432
numberOfReRenders += 1;
433
if (__DEV__) {
434
// Even when hot reloading, allow dependencies to stabilize
@@ -447,14 +448,11 @@ export function renderWithHooks(
448
}
449
450
ReactCurrentDispatcher.current = __DEV__
450
- ? HooksDispatcherOnUpdateInDEV
451
- : HooksDispatcherOnUpdate;
451
+ ? HooksDispatcherOnRerenderInDEV
452
+ : HooksDispatcherOnRerender;
453
454
children = Component(props, refOrContext);
455
} while (didScheduleRenderPhaseUpdate);
455
-
456
- renderPhaseUpdates = null;
457
- numberOfReRenders = 0;
456
}
457
458
// We can assume the previous dispatcher is always this one, since we set it
@@ -484,8 +482,6 @@ export function renderWithHooks(
482
483
// These were reset above
484
// didScheduleRenderPhaseUpdate = false;
487
- // renderPhaseUpdates = null;
488
- // numberOfReRenders = 0;
485
486
invariant(
487
!didRenderTooFewHooks,
@@ -508,14 +504,31 @@ export function bailoutHooks(
504
}
505
}
506
511
-export function resetHooks(): void {
507
+export function resetHooksAfterThrow(): void {
508
// We can assume the previous dispatcher is always this one, since we set it
509
// at the beginning of the render phase and there's no re-entrancy.
510
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
511
516
- // This is used to reset the state of this module when a component throws.
517
- // It's also called inside mountIndeterminateComponent if we determine the
518
- // component is a module-style component.
512
+ if (didScheduleRenderPhaseUpdate) {
513
+ const current = (currentlyRenderingFiber: any).alternate;
514
+ if (current !== null) {
515
+ // There were render phase updates. These are only valid for this render
516
+ // pass, which we are now aborting. Remove the updates from the queues so
517
+ // they do not persist to the next render. We already did a single pass
518
+ // through the whole list of hooks, so we know that any pending updates
519
+ // must have been dispatched during the render phase. The ones that were
520
+ // dispatched before we started rendering were already transferred to the
521
+ // current hook's queue.
522
+ let hook: Hook | null = current.memoizedState;
523
+ while (hook !== null) {
524
+ const queue = hook.queue;
525
+ if (queue !== null) {
526
+ queue.pending = null;
527
+ }
528
+ hook = hook.next;
529
+ }
530
+ }
531
+ }
532
533
renderExpirationTime = NoWork;
534
currentlyRenderingFiber = (null: any);
@@ -531,8 +544,6 @@ export function resetHooks(): void {
544
}
545
546
didScheduleRenderPhaseUpdate = false;
534
- renderPhaseUpdates = null;
535
- numberOfReRenders = 0;
547
}
548
549
function mountWorkInProgressHook(): Hook {
@@ -668,49 +679,6 @@ function updateReducer<S, I, A>(
679
680
queue.lastRenderedReducer = reducer;
681
671
- if (numberOfReRenders > 0) {
672
- // This is a re-render. Apply the new render phase updates to the previous
673
- // work-in-progress hook.
674
- const dispatch: Dispatch<A> = (queue.dispatch: any);
675
- if (renderPhaseUpdates !== null) {
676
- // Render phase updates are stored in a map of queue -> linked list
677
- const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
678
- if (firstRenderPhaseUpdate !== undefined) {
679
- renderPhaseUpdates.delete(queue);
680
- let newState = hook.memoizedState;
681
- let update = firstRenderPhaseUpdate;
682
- do {
683
- // Process this render phase update. We don't have to check the
684
- // priority because it will always be the same as the current
685
- // render's.
686
- const action = update.action;
687
- newState = reducer(newState, action);
688
- update = update.next;
689
- } while (update !== null);
690
-
691
- // Mark that the fiber performed work, but only if the new state is
692
- // different from the current state.
693
- if (!is(newState, hook.memoizedState)) {
694
- markWorkInProgressReceivedUpdate();
695
- }
696
-
697
- hook.memoizedState = newState;
698
- // Don't persist the state accumulated from the render phase updates to
699
- // the base state unless the queue is empty.
700
- // TODO: Not sure if this is the desired semantics, but it's what we
701
- // do for gDSFP. I can't remember why.
702
- if (hook.baseQueue === null) {
703
- hook.baseState = newState;
704
- }
705
-
706
- queue.lastRenderedState = newState;
707
-
708
- return [newState, dispatch];
709
- }
710
- }
711
- return [hook.memoizedState, dispatch];
712
- }
713
-
682
const current: Hook = (currentHook: any);
683
684
// The last rebase update that is NOT part of the base state.
@@ -828,6 +796,60 @@ function updateReducer<S, I, A>(
796
return [hook.memoizedState, dispatch];
797
}
798
799
+function rerenderReducer<S, I, A>(
800
+ reducer: (S, A) => S,
801
+ initialArg: I,
802
+ init?: I => S,
803
+): [S, Dispatch<A>] {
804
+ const hook = updateWorkInProgressHook();
805
+ const queue = hook.queue;
806
+ invariant(
807
+ queue !== null,
808
+ 'Should have a queue. This is likely a bug in React. Please file an issue.',
809
+ );
810
+
811
+ queue.lastRenderedReducer = reducer;
812
+
813
+ // This is a re-render. Apply the new render phase updates to the previous
814
+ // work-in-progress hook.
815
+ const dispatch: Dispatch<A> = (queue.dispatch: any);
816
+ const lastRenderPhaseUpdate = queue.pending;
817
+ let newState = hook.memoizedState;
818
+ if (lastRenderPhaseUpdate !== null) {
819
+ // The queue doesn't persist past this render pass.
820
+ queue.pending = null;
821
+
822
+ const firstRenderPhaseUpdate = lastRenderPhaseUpdate.next;
823
+ let update = firstRenderPhaseUpdate;
824
+ do {
825
+ // Process this render phase update. We don't have to check the
826
+ // priority because it will always be the same as the current
827
+ // render's.
828
+ const action = update.action;
829
+ newState = reducer(newState, action);
830
+ update = update.next;
831
+ } while (update !== firstRenderPhaseUpdate);
832
+
833
+ // Mark that the fiber performed work, but only if the new state is
834
+ // different from the current state.
835
+ if (!is(newState, hook.memoizedState)) {
836
+ markWorkInProgressReceivedUpdate();
837
+ }
838
+
839
+ hook.memoizedState = newState;
840
+ // Don't persist the state accumulated from the render phase updates to
841
+ // the base state unless the queue is empty.
842
+ // TODO: Not sure if this is the desired semantics, but it's what we
843
+ // do for gDSFP. I can't remember why.
844
+ if (hook.baseQueue === null) {
845
+ hook.baseState = newState;
846
+ }
847
+
848
+ queue.lastRenderedState = newState;
849
+ }
850
+ return [newState, dispatch];
851
+}
852
+
853
function mountState<S>(
854
initialState: (() => S) | S,
855
): [S, Dispatch<BasicStateAction<S>>] {
@@ -858,6 +880,12 @@ function updateState<S>(
880
return updateReducer(basicStateReducer, (initialState: any));
881
}
882
883
+function rerenderState<S>(
884
+ initialState: (() => S) | S,
885
+): [S, Dispatch<BasicStateAction<S>>] {
886
+ return rerenderReducer(basicStateReducer, (initialState: any));
887
+}
888
+
889
function pushEffect(tag, create, destroy, deps) {
890
const effect: Effect = {
891
tag,
@@ -1227,12 +1255,6 @@ function dispatchAction<S, A>(
1255
queue: UpdateQueue<S, A>,
1256
action: A,
1257
) {
1230
- invariant(
1231
- numberOfReRenders < RE_RENDER_LIMIT,
1232
- 'Too many re-renders. React limits the number of renders to prevent ' +
1233
- 'an infinite loop.',
1234
- );
1235
-
1258
if (__DEV__) {
1259
if (typeof arguments[3] === 'function') {
1260
warning(
@@ -1243,6 +1265,38 @@ function dispatchAction<S, A>(
1265
}
1266
}
1267
1268
+ const currentTime = requestCurrentTimeForUpdate();
1269
+ const suspenseConfig = requestCurrentSuspenseConfig();
1270
+ const expirationTime = computeExpirationForFiber(
1271
+ currentTime,
1272
+ fiber,
1273
+ suspenseConfig,
1274
+ );
1275
+
1276
+ const update: Update<S, A> = {
1277
+ expirationTime,
1278
+ suspenseConfig,
1279
+ action,
1280
+ eagerReducer: null,
1281
+ eagerState: null,
1282
+ next: (null: any),
1283
+ };
1284
+
1285
+ if (__DEV__) {
1286
+ update.priority = getCurrentPriorityLevel();
1287
+ }
1288
+
1289
+ // Append the update to the end of the list.
1290
+ const pending = queue.pending;
1291
+ if (pending === null) {
1292
+ // This is the first update. Create a circular list.
1293
+ update.next = update;
1294
+ } else {
1295
+ update.next = pending.next;
1296
+ pending.next = update;
1297
+ }
1298
+ queue.pending = update;
1299
+
1300
const alternate = fiber.alternate;
1301
if (
1302
fiber === currentlyRenderingFiber ||
@@ -1252,64 +1306,8 @@ function dispatchAction<S, A>(
1306
// queue -> linked list of updates. After this render pass, we'll restart
1307
// and apply the stashed updates on top of the work-in-progress hook.
1308
didScheduleRenderPhaseUpdate = true;
1255
- const update: Update<S, A> = {
1256
- expirationTime: renderExpirationTime,
1257
- suspenseConfig: null,
1258
- action,
1259
- eagerReducer: null,
1260
- eagerState: null,
1261
- next: (null: any),
1262
- };
1263
- if (__DEV__) {
1264
- update.priority = getCurrentPriorityLevel();
1265
- }
1266
- if (renderPhaseUpdates === null) {
1267
- renderPhaseUpdates = new Map();
1268
- }
1269
- const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
1270
- if (firstRenderPhaseUpdate === undefined) {
1271
- renderPhaseUpdates.set(queue, update);
1272
- } else {
1273
- // Append the update to the end of the list.
1274
- let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
1275
- while (lastRenderPhaseUpdate.next !== null) {
1276
- lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
1277
- }
1278
- lastRenderPhaseUpdate.next = update;
1279
- }
1309
+ update.expirationTime = renderExpirationTime;
1310
} else {
1281
- const currentTime = requestCurrentTimeForUpdate();
1282
- const suspenseConfig = requestCurrentSuspenseConfig();
1283
- const expirationTime = computeExpirationForFiber(
1284
- currentTime,
1285
- fiber,
1286
- suspenseConfig,
1287
- );
1288
-
1289
- const update: Update<S, A> = {
1290
- expirationTime,
1291
- suspenseConfig,
1292
- action,
1293
- eagerReducer: null,
1294
- eagerState: null,
1295
- next: (null: any),
1296
- };
1297
-
1298
- if (__DEV__) {
1299
- update.priority = getCurrentPriorityLevel();
1300
- }
1301
-
1302
- // Append the update to the end of the list.
1303
- const pending = queue.pending;
1304
- if (pending === null) {
1305
- // This is the first update. Create a circular list.
1306
- update.next = update;
1307
- } else {
1308
- update.next = pending.next;
1309
- pending.next = update;
1310
- }
1311
- queue.pending = update;
1312
-
1311
if (
1312
fiber.expirationTime === NoWork &&
1313
(alternate === null || alternate.expirationTime === NoWork)
@@ -1414,11 +1412,31 @@ const HooksDispatcherOnUpdate: Dispatcher = {
1412
useTransition: updateTransition,
1413
};
1414
1415
+const HooksDispatcherOnRerender: Dispatcher = {
1416
+ readContext,
1417
+
1418
+ useCallback: updateCallback,
1419
+ useContext: readContext,
1420
+ useEffect: updateEffect,
1421
+ useImperativeHandle: updateImperativeHandle,
1422
+ useLayoutEffect: updateLayoutEffect,
1423
+ useMemo: updateMemo,
1424
+ useReducer: rerenderReducer,
1425
+ useRef: updateRef,
1426
+ useState: rerenderState,
1427
+ useDebugValue: updateDebugValue,
1428
+ useResponder: createResponderListener,
1429
+ useDeferredValue: updateDeferredValue,
1430
+ useTransition: updateTransition,
1431
+};
1432
+
1433
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
1434
let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
1435
let HooksDispatcherOnUpdateInDEV: Dispatcher | null = null;
1436
+let HooksDispatcherOnRerenderInDEV: Dispatcher | null = null;
1437
let InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher | null = null;
1438
let InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher | null = null;
1439
+let InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher | null = null;
1440
1441
if (__DEV__) {
1442
const warnInvalidContextAccess = () => {
@@ -1795,6 +1813,123 @@ if (__DEV__) {
1813
},
1814
};
1815
1816
+ HooksDispatcherOnRerenderInDEV = {
1817
+ readContext<T>(
1818
+ context: ReactContext<T>,
1819
+ observedBits: void | number | boolean,
1820
+ ): T {
1821
+ return readContext(context, observedBits);
1822
+ },
1823
+
1824
+ useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
1825
+ currentHookNameInDev = 'useCallback';
1826
+ updateHookTypesDev();
1827
+ return updateCallback(callback, deps);
1828
+ },
1829
+ useContext<T>(
1830
+ context: ReactContext<T>,
1831
+ observedBits: void | number | boolean,
1832
+ ): T {
1833
+ currentHookNameInDev = 'useContext';
1834
+ updateHookTypesDev();
1835
+ return readContext(context, observedBits);
1836
+ },
1837
+ useEffect(
1838
+ create: () => (() => void) | void,
1839
+ deps: Array<mixed> | void | null,
1840
+ ): void {
1841
+ currentHookNameInDev = 'useEffect';
1842
+ updateHookTypesDev();
1843
+ return updateEffect(create, deps);
1844
+ },
1845
+ useImperativeHandle<T>(
1846
+ ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
1847
+ create: () => T,
1848
+ deps: Array<mixed> | void | null,
1849
+ ): void {
1850
+ currentHookNameInDev = 'useImperativeHandle';
1851
+ updateHookTypesDev();
1852
+ return updateImperativeHandle(ref, create, deps);
1853
+ },
1854
+ useLayoutEffect(
1855
+ create: () => (() => void) | void,
1856
+ deps: Array<mixed> | void | null,
1857
+ ): void {
1858
+ currentHookNameInDev = 'useLayoutEffect';
1859
+ updateHookTypesDev();
1860
+ return updateLayoutEffect(create, deps);
1861
+ },
1862
+ useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
1863
+ currentHookNameInDev = 'useMemo';
1864
+ updateHookTypesDev();
1865
+ const prevDispatcher = ReactCurrentDispatcher.current;
1866
+ ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnRerenderInDEV;
1867
+ try {
1868
+ return updateMemo(create, deps);
1869
+ } finally {
1870
+ ReactCurrentDispatcher.current = prevDispatcher;
1871
+ }
1872
+ },
1873
+ useReducer<S, I, A>(
1874
+ reducer: (S, A) => S,
1875
+ initialArg: I,
1876
+ init?: I => S,
1877
+ ): [S, Dispatch<A>] {
1878
+ currentHookNameInDev = 'useReducer';
1879
+ updateHookTypesDev();
1880
+ const prevDispatcher = ReactCurrentDispatcher.current;
1881
+ ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnRerenderInDEV;
1882
+ try {
1883
+ return rerenderReducer(reducer, initialArg, init);
1884
+ } finally {
1885
+ ReactCurrentDispatcher.current = prevDispatcher;
1886
+ }
1887
+ },
1888
+ useRef<T>(initialValue: T): {current: T} {
1889
+ currentHookNameInDev = 'useRef';
1890
+ updateHookTypesDev();
1891
+ return updateRef(initialValue);
1892
+ },
1893
+ useState<S>(
1894
+ initialState: (() => S) | S,
1895
+ ): [S, Dispatch<BasicStateAction<S>>] {
1896
+ currentHookNameInDev = 'useState';
1897
+ updateHookTypesDev();
1898
+ const prevDispatcher = ReactCurrentDispatcher.current;
1899
+ ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnRerenderInDEV;
1900
+ try {
1901
+ return rerenderState(initialState);
1902
+ } finally {
1903
+ ReactCurrentDispatcher.current = prevDispatcher;
1904
+ }
1905
+ },
1906
+ useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
1907
+ currentHookNameInDev = 'useDebugValue';
1908
+ updateHookTypesDev();
1909
+ return updateDebugValue(value, formatterFn);
1910
+ },
1911
+ useResponder<E, C>(
1912
+ responder: ReactEventResponder<E, C>,
1913
+ props,
1914
+ ): ReactEventResponderListener<E, C> {
1915
+ currentHookNameInDev = 'useResponder';
1916
+ updateHookTypesDev();
1917
+ return createResponderListener(responder, props);
1918
+ },
1919
+ useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T {
1920
+ currentHookNameInDev = 'useDeferredValue';
1921
+ updateHookTypesDev();
1922
+ return updateDeferredValue(value, config);
1923
+ },
1924
+ useTransition(
1925
+ config: SuspenseConfig | void | null,
1926
+ ): [(() => void) => void, boolean] {
1927
+ currentHookNameInDev = 'useTransition';
1928
+ updateHookTypesDev();
1929
+ return updateTransition(config);
1930
+ },
1931
+ };
1932
+
1933
InvalidNestedHooksDispatcherOnMountInDEV = {
1934
readContext<T>(
1935
context: ReactContext<T>,
@@ -2056,4 +2191,135 @@ if (__DEV__) {
2191
return updateTransition(config);
2192
},
2193
};
2194
+
2195
+ InvalidNestedHooksDispatcherOnRerenderInDEV = {
2196
+ readContext<T>(
2197
+ context: ReactContext<T>,
2198
+ observedBits: void | number | boolean,
2199
+ ): T {
2200
+ warnInvalidContextAccess();
2201
+ return readContext(context, observedBits);
2202
+ },
2203
+
2204
+ useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
2205
+ currentHookNameInDev = 'useCallback';
2206
+ warnInvalidHookAccess();
2207
+ updateHookTypesDev();
2208
+ return updateCallback(callback, deps);
2209
+ },
2210
+ useContext<T>(
2211
+ context: ReactContext<T>,
2212
+ observedBits: void | number | boolean,
2213
+ ): T {
2214
+ currentHookNameInDev = 'useContext';
2215
+ warnInvalidHookAccess();
2216
+ updateHookTypesDev();
2217
+ return readContext(context, observedBits);
2218
+ },
2219
+ useEffect(
2220
+ create: () => (() => void) | void,
2221
+ deps: Array<mixed> | void | null,
2222
+ ): void {
2223
+ currentHookNameInDev = 'useEffect';
2224
+ warnInvalidHookAccess();
2225
+ updateHookTypesDev();
2226
+ return updateEffect(create, deps);
2227
+ },
2228
+ useImperativeHandle<T>(
2229
+ ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
2230
+ create: () => T,
2231
+ deps: Array<mixed> | void | null,
2232
+ ): void {
2233
+ currentHookNameInDev = 'useImperativeHandle';
2234
+ warnInvalidHookAccess();
2235
+ updateHookTypesDev();
2236
+ return updateImperativeHandle(ref, create, deps);
2237
+ },
2238
+ useLayoutEffect(
2239
+ create: () => (() => void) | void,
2240
+ deps: Array<mixed> | void | null,
2241
+ ): void {
2242
+ currentHookNameInDev = 'useLayoutEffect';
2243
+ warnInvalidHookAccess();
2244
+ updateHookTypesDev();
2245
+ return updateLayoutEffect(create, deps);
2246
+ },
2247
+ useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
2248
+ currentHookNameInDev = 'useMemo';
2249
+ warnInvalidHookAccess();
2250
+ updateHookTypesDev();
2251
+ const prevDispatcher = ReactCurrentDispatcher.current;
2252
+ ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
2253
+ try {
2254
+ return updateMemo(create, deps);
2255
+ } finally {
2256
+ ReactCurrentDispatcher.current = prevDispatcher;
2257
+ }
2258
+ },
2259
+ useReducer<S, I, A>(
2260
+ reducer: (S, A) => S,
2261
+ initialArg: I,
2262
+ init?: I => S,
2263
+ ): [S, Dispatch<A>] {
2264
+ currentHookNameInDev = 'useReducer';
2265
+ warnInvalidHookAccess();
2266
+ updateHookTypesDev();
2267
+ const prevDispatcher = ReactCurrentDispatcher.current;
2268
+ ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
2269
+ try {
2270
+ return rerenderReducer(reducer, initialArg, init);
2271
+ } finally {
2272
+ ReactCurrentDispatcher.current = prevDispatcher;
2273
+ }
2274
+ },
2275
+ useRef<T>(initialValue: T): {current: T} {
2276
+ currentHookNameInDev = 'useRef';
2277
+ warnInvalidHookAccess();
2278
+ updateHookTypesDev();
2279
+ return updateRef(initialValue);
2280
+ },
2281
+ useState<S>(
2282
+ initialState: (() => S) | S,
2283
+ ): [S, Dispatch<BasicStateAction<S>>] {
2284
+ currentHookNameInDev = 'useState';
2285
+ warnInvalidHookAccess();
2286
+ updateHookTypesDev();
2287
+ const prevDispatcher = ReactCurrentDispatcher.current;
2288
+ ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
2289
+ try {
2290
+ return rerenderState(initialState);
2291
+ } finally {
2292
+ ReactCurrentDispatcher.current = prevDispatcher;
2293
+ }
2294
+ },
2295
+ useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
2296
+ currentHookNameInDev = 'useDebugValue';
2297
+ warnInvalidHookAccess();
2298
+ updateHookTypesDev();
2299
+ return updateDebugValue(value, formatterFn);
2300
+ },
2301
+ useResponder<E, C>(
2302
+ responder: ReactEventResponder<E, C>,
2303
+ props,
2304
+ ): ReactEventResponderListener<E, C> {
2305
+ currentHookNameInDev = 'useResponder';
2306
+ warnInvalidHookAccess();
2307
+ updateHookTypesDev();
2308
+ return createResponderListener(responder, props);
2309
+ },
2310
+ useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T {
2311
+ currentHookNameInDev = 'useDeferredValue';
2312
+ warnInvalidHookAccess();
2313
+ updateHookTypesDev();
2314
+ return updateDeferredValue(value, config);
2315
+ },
2316
+ useTransition(
2317
+ config: SuspenseConfig | void | null,
2318
+ ): [(() => void) => void, boolean] {
2319
+ currentHookNameInDev = 'useTransition';
2320
+ warnInvalidHookAccess();
2321
+ updateHookTypesDev();
2322
+ return updateTransition(config);
2323
+ },
2324
+ };
2325
}
packages/react-reconciler/src/ReactFiberWorkLoop.js
+3
-3
@@ -140,7 +140,7 @@ import {
140
} from './ReactFiberCommitWork';
141
import {enqueueUpdate} from './ReactUpdateQueue';
142
import {resetContextDependencies} from './ReactFiberNewContext';
143
-import {resetHooks, ContextOnlyDispatcher} from './ReactFiberHooks';
143
+import {resetHooksAfterThrow, ContextOnlyDispatcher} from './ReactFiberHooks';
144
import {createCapturedValue} from './ReactCapturedValue';
145
146
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) {
@@ -2635,7 +2635,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
2635
// Keep this code in sync with handleError; any changes here must have
2636
// corresponding changes there.
2637
resetContextDependencies();
2638
- resetHooks();
2638
+ resetHooksAfterThrow();
2639
// Don't reset current debug fiber, since we're about to work on the
2640
// same fiber again.
2641
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+44
@@ -542,6 +542,50 @@ describe('ReactHooksWithNoopRenderer', () => {
542
]);
543
expect(ReactNoop.getChildren()).toEqual([span(22)]);
544
});
545
+
546
+ it('discards render phase updates if something suspends', () => {
547
+ const thenable = {then() {}};
548
+ function Foo({signal}) {
549
+ return (
550
+ <Suspense fallback="Loading...">
551
+ <Bar signal={signal} />
552
+ </Suspense>
553
+ );
554
+ }
555
+
556
+ function Bar({signal: newSignal}) {
557
+ let [counter, setCounter] = useState(0);
558
+ let [signal, setSignal] = useState(true);
559
+
560
+ // Increment a counter every time the signal changes
561
+ if (signal !== newSignal) {
562
+ setCounter(c => c + 1);
563
+ setSignal(newSignal);
564
+ if (counter === 0) {
565
+ // We're suspending during a render that includes render phase
566
+ // updates. Those updates should not persist to the next render.
567
+ Scheduler.unstable_yieldValue('Suspend!');
568
+ throw thenable;
569
+ }
570
+ }
571
+
572
+ return <Text text={counter} />;
573
+ }
574
+
575
+ const root = ReactNoop.createRoot();
576
+ root.render(<Foo signal={true} />);
577
+
578
+ expect(Scheduler).toFlushAndYield([0]);
579
+ expect(root).toMatchRenderedOutput(<span prop={0} />);
580
+
581
+ root.render(<Foo signal={false} />);
582
+ expect(Scheduler).toFlushAndYield(['Suspend!']);
583
+ expect(root).toMatchRenderedOutput(<span prop={0} />);
584
+
585
+ // Rendering again should suspend again.
586
+ root.render(<Foo signal={false} />);
587
+ expect(Scheduler).toFlushAndYield(['Suspend!']);
588
+ });
589
});
590
591
describe('useReducer', () => {