@samitouri / QOS-React / commits / 95bd7aad7d

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', () => {