@samitouri / QOS-React / commits / 8a347ed024

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