@samitouri / QOS-React / commits / cb1ff430e8

Phased dispatcher (#14701)

* Move DEV-only function right above where it's used I don't like looking at this top-level function #petty * Use different dispatchers for functions & classes Classes support readContext, but not any of the other dispatcher methods. Function support all methods. This is a more robust version of our previous strategy of checking whether `currentlyRenderingFiber` is null. As a next step, we can use a separate dispatcher for each phase of the render cycle (mount versus update). * Use separate dispatchers for mount and update * Remove mount code from update path Deletes mount-specific code from the update path, since it should be unreachable. To continue supporting progressive enhancement (mounting new hooks at the end of the list), we detect when there are no more current hooks and switch back to the mount dispatcher. Progressive enhancement isn't officially supported yet, so it will continue to warn. * Factoring nits * Fix Flow Had to cheat more than I would like * More Flow nits * Switch back to using a special dispatcher for nested hooks in DEV In order for this strategy to work, I had to revert progressive enhancement support (appending hooks to the end). It was previously a warning but now it results in an error. We'll reconsider later. * Always pass args to updateState and updateReducer Even though the extra args are only used on mount, to ensure type consistency.

Andrew Clark committed Jan 29, 2019 at 16:32 UTC cb1ff430e8c473a8a6bddd592106891251bbf5bf
10 files changed +1001 -557
packages/react-debug-tools/src/ReactDebugHooks.js
+1 -1
@@ -10,7 +10,7 @@
10 import type {ReactContext, ReactProviderType} from 'shared/ReactTypes';
11 import type {Fiber} from 'react-reconciler/src/ReactFiber';
12 import type {Hook} from 'react-reconciler/src/ReactFiberHooks';
13 -import typeof {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberDispatcher';
13 +import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberHooks';
14
15 import ErrorStackParser from 'error-stack-parser';
16 import ReactSharedInternals from 'shared/ReactSharedInternals';
packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js
+17 -13
@@ -432,21 +432,25 @@ describe('ReactDOMServerHooks', () => {
432 expect(domNode.textContent).toEqual('hi');
433 });
434
435 - itRenders('with a warning for useRef inside useReducer', async render => {
436 - function App() {
437 - const [value, dispatch] = useReducer((state, action) => {
438 - useRef(0);
439 - return state + 1;
440 - }, 0);
441 - if (value === 0) {
442 - dispatch();
435 + itThrowsWhenRendering(
436 + 'with a warning for useRef inside useReducer',
437 + async render => {
438 + function App() {
439 + const [value, dispatch] = useReducer((state, action) => {
440 + useRef(0);
441 + return state + 1;
442 + }, 0);
443 + if (value === 0) {
444 + dispatch();
445 + }
446 + return value;
447 }
444 - return value;
445 - }
448
447 - const domNode = await render(<App />, 1);
448 - expect(domNode.textContent).toEqual('1');
449 - });
449 + const domNode = await render(<App />, 1);
450 + expect(domNode.textContent).toEqual('1');
451 + },
452 + 'Rendered more hooks than during the previous render',
453 + );
454
455 itRenders('with a warning for useRef inside useState', async render => {
456 function App() {
packages/react-dom/src/server/ReactPartialRendererHooks.js
+4 -1
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import typeof {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberDispatcher';
10 +import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberHooks';
11 import type {ThreadID} from './ReactThreadIDAllocator';
12 import type {ReactContext} from 'shared/ReactTypes';
13
@@ -113,6 +113,9 @@ function areHookInputsEqual(
113 }
114
115 function createHook(): Hook {
116 + if (numberOfReRenders > 0) {
117 + invariant(false, 'Rendered more hooks than during the previous render');
118 + }
119 return {
120 memoizedState: null,
121 queue: null,
packages/react-reconciler/src/ReactFiberDispatcher.js deleted
-36
@@ -1,36 +0,0 @@
1 -/**
2 - * Copyright (c) Facebook, Inc. and its affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @flow
8 - */
9 -
10 -import {readContext} from './ReactFiberNewContext';
11 -import {
12 - useCallback,
13 - useContext,
14 - useEffect,
15 - useImperativeHandle,
16 - useDebugValue,
17 - useLayoutEffect,
18 - useMemo,
19 - useReducer,
20 - useRef,
21 - useState,
22 -} from './ReactFiberHooks';
23 -
24 -export const Dispatcher = {
25 - readContext,
26 - useCallback,
27 - useContext,
28 - useEffect,
29 - useImperativeHandle,
30 - useDebugValue,
31 - useLayoutEffect,
32 - useMemo,
33 - useReducer,
34 - useRef,
35 - useState,
36 -};
packages/react-reconciler/src/ReactFiberHooks.js
+936 -473
@@ -8,16 +8,15 @@
8 */
9
10 import type {ReactContext} from 'shared/ReactTypes';
11 +import type {SideEffectTag} from 'shared/ReactSideEffectTags';
12 import type {Fiber} from './ReactFiber';
13 import type {ExpirationTime} from './ReactFiberExpirationTime';
14 import type {HookEffectTag} from './ReactHookEffectTags';
15
16 +import ReactSharedInternals from 'shared/ReactSharedInternals';
17 +
18 import {NoWork} from './ReactFiberExpirationTime';
16 -import {
17 - readContext,
18 - enterDisallowedContextReadInDEV,
19 - exitDisallowedContextReadInDEV,
20 -} from './ReactFiberNewContext';
19 +import {readContext} from './ReactFiberNewContext';
20 import {
21 Update as UpdateEffect,
22 Passive as PassiveEffect,
@@ -42,6 +41,36 @@ import getComponentName from 'shared/getComponentName';
41 import is from 'shared/objectIs';
42 import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork';
43
44 +const {ReactCurrentDispatcher} = ReactSharedInternals;
45 +
46 +export type Dispatcher = {
47 + readContext<T>(
48 + context: ReactContext<T>,
49 + observedBits: void | number | boolean,
50 + ): T,
51 + useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
52 + useReducer<S, A>(
53 + reducer: (S, A) => S,
54 + initialState: S,
55 + initialAction: A | void | null,
56 + ): [S, Dispatch<A>],
57 + useContext<T>(
58 + context: ReactContext<T>,
59 + observedBits: void | number | boolean,
60 + ): T,
61 + useRef<T>(initialValue: T): {current: T},
62 + useEffect(create: () => mixed, deps: Array<mixed> | void | null): void,
63 + useLayoutEffect(create: () => mixed, deps: Array<mixed> | void | null): void,
64 + useCallback<T>(callback: T, deps: Array<mixed> | void | null): T,
65 + useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T,
66 + useImperativeHandle<T>(
67 + ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
68 + create: () => T,
69 + deps: Array<mixed> | void | null,
70 + ): void,
71 + useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void,
72 +};
73 +
74 type Update<S, A> = {
75 expirationTime: ExpirationTime,
76 action: A,
@@ -72,7 +101,6 @@ type HookType =
101 // the first instance of a hook mismatch in a component,
102 // represented by a portion of its stacktrace
103 let currentHookMismatchInDev = null;
75 -let isInHookUserCodeInDev = false;
104
105 let didWarnAboutMismatchedHooksForComponent;
106 if (__DEV__) {
@@ -121,11 +149,14 @@ let currentlyRenderingFiber: Fiber | null = null;
149 // work-in-progress fiber.
150 let firstCurrentHook: Hook | null = null;
151 let currentHook: Hook | null = null;
152 +let nextCurrentHook: Hook | null = null;
153 let firstWorkInProgressHook: Hook | null = null;
154 let workInProgressHook: Hook | null = null;
155 +let nextWorkInProgressHook: Hook | null = null;
156
157 let remainingExpirationTime: ExpirationTime = NoWork;
158 let componentUpdateQueue: FunctionComponentUpdateQueue | null = null;
159 +let sideEffectTag: SideEffectTag = 0;
160
161 // Updates scheduled during render will trigger an immediate re-render at the
162 // end of the current pass. We can't store these updates on the normal queue,
@@ -143,29 +174,17 @@ let renderPhaseUpdates: Map<
174 Update<any, any>,
175 > | null = null;
176 // Counter to prevent infinite loops.
146 -let numberOfReRenders: number = -1;
177 +let numberOfReRenders: number = 0;
178 const RE_RENDER_LIMIT = 25;
179
180 // In DEV, this is the name of the currently executing primitive hook
181 let currentHookNameInDev: ?HookType = null;
182
152 -function resolveCurrentlyRenderingFiber(): Fiber {
183 +function throwInvalidHookError() {
184 invariant(
154 - currentlyRenderingFiber !== null,
185 + false,
186 'Hooks can only be called inside the body of a function component.',
187 );
157 - if (__DEV__) {
158 - // Check if we're inside Hooks like useMemo(). DEV-only for perf.
159 - // TODO: we can make a better warning message with currentHookNameInDev
160 - // if we also make sure it's consistently assigned in the right order.
161 - warning(
162 - !isInHookUserCodeInDev,
163 - 'Hooks can only be called inside the body of a function component. ' +
164 - 'Do not call Hooks inside other Hooks. For more information, see ' +
165 - 'https://fb.me/rules-of-hooks',
166 - );
167 - }
168 - return currentlyRenderingFiber;
188 }
189
190 function areHookInputsEqual(
@@ -210,18 +229,6 @@ function areHookInputsEqual(
229 return true;
230 }
231
213 -// till we have String::padEnd, a small function to
214 -// right-pad strings with spaces till a minimum length
215 -function padEndSpaces(string: string, length: number) {
216 - if (__DEV__) {
217 - if (string.length >= length) {
218 - return string;
219 - } else {
220 - return string + ' ' + new Array(length - string.length).join(' ');
221 - }
222 - }
223 -}
224 -
232 function flushHookMismatchWarnings() {
233 // we'll show the diff of the low level hooks,
234 // and a stack trace so the dev can locate where
@@ -254,6 +261,13 @@ function flushHookMismatchWarnings() {
261 .concat(' Previous render'.length),
262 );
263
264 + const padEndSpaces = (string, length) => {
265 + if (string.length >= length) {
266 + return string;
267 + }
268 + return string + ' ' + new Array(length - string.length).join(' ');
269 + };
270 +
271 let hookStackHeader =
272 ((padEndSpaces(' Previous render', columnLength): any): string) +
273 ' Next render\n';
@@ -309,12 +323,8 @@ export function renderWithHooks(
323 ): any {
324 renderExpirationTime = nextRenderExpirationTime;
325 currentlyRenderingFiber = workInProgress;
312 - firstCurrentHook = current !== null ? current.memoizedState : null;
313 -
314 - if (__DEV__) {
315 - exitDisallowedContextReadInDEV();
316 - isInHookUserCodeInDev = false;
317 - }
326 + firstCurrentHook = nextCurrentHook =
327 + current !== null ? current.memoizedState : null;
328
329 // The following should have already been reset
330 // currentHook = null;
@@ -325,45 +335,63 @@ export function renderWithHooks(
335
336 // didScheduleRenderPhaseUpdate = false;
337 // renderPhaseUpdates = null;
328 - // numberOfReRenders = -1;
338 + // numberOfReRenders = 0;
339 + // sideEffectTag = 0;
340
330 - let children;
331 - do {
332 - didScheduleRenderPhaseUpdate = false;
333 - numberOfReRenders += 1;
341 + if (__DEV__) {
342 + ReactCurrentDispatcher.current =
343 + nextCurrentHook === null
344 + ? HooksDispatcherOnMountInDEV
345 + : HooksDispatcherOnUpdateInDEV;
346 + } else {
347 + ReactCurrentDispatcher.current =
348 + nextCurrentHook === null
349 + ? HooksDispatcherOnMount
350 + : HooksDispatcherOnUpdate;
351 + }
352
335 - // Start over from the beginning of the list
336 - currentHook = null;
337 - workInProgressHook = null;
338 - componentUpdateQueue = null;
353 + let children = Component(props, refOrContext);
354
340 - children = Component(props, refOrContext);
355 + if (didScheduleRenderPhaseUpdate) {
356 + do {
357 + didScheduleRenderPhaseUpdate = false;
358 + numberOfReRenders += 1;
359
342 - if (__DEV__) {
343 - if (
344 - current !== null &&
345 - workInProgressHook !== null &&
346 - currentHook === null
347 - ) {
348 - warning(
349 - false,
350 - '%s: Rendered more hooks than during the previous render. This is ' +
351 - 'not currently supported and may lead to unexpected behavior.',
352 - getComponentName(Component),
353 - );
354 - }
355 - flushHookMismatchWarnings();
356 - }
357 - } while (didScheduleRenderPhaseUpdate);
360 + // Start over from the beginning of the list
361 + firstCurrentHook = nextCurrentHook =
362 + current !== null ? current.memoizedState : null;
363 + nextWorkInProgressHook = firstWorkInProgressHook;
364
359 - renderPhaseUpdates = null;
360 - numberOfReRenders = -1;
365 + currentHook = null;
366 + workInProgressHook = null;
367 + componentUpdateQueue = null;
368 +
369 + ReactCurrentDispatcher.current = __DEV__
370 + ? HooksDispatcherOnUpdateInDEV
371 + : HooksDispatcherOnUpdate;
372 +
373 + children = Component(props, refOrContext);
374 + } while (didScheduleRenderPhaseUpdate);
375 +
376 + renderPhaseUpdates = null;
377 + numberOfReRenders = 0;
378 + }
379 +
380 + if (__DEV__) {
381 + flushHookMismatchWarnings();
382 + currentHookNameInDev = null;
383 + }
384 +
385 + // We can assume the previous dispatcher is always this one, since we set it
386 + // at the beginning of the render phase and there's no re-entrancy.
387 + ReactCurrentDispatcher.current = ContextOnlyDispatcher;
388
389 const renderedWork: Fiber = (currentlyRenderingFiber: any);
390
391 renderedWork.memoizedState = firstWorkInProgressHook;
392 renderedWork.expirationTime = remainingExpirationTime;
366 - renderedWork.updateQueue = componentUpdateQueue;
393 + renderedWork.updateQueue = (componentUpdateQueue: any);
394 + renderedWork.effectTag |= sideEffectTag;
395
396 const didRenderTooFewHooks =
397 currentHook !== null && currentHook.next !== null;
@@ -373,20 +401,19 @@ export function renderWithHooks(
401
402 firstCurrentHook = null;
403 currentHook = null;
404 + nextCurrentHook = null;
405 firstWorkInProgressHook = null;
406 workInProgressHook = null;
407 + nextWorkInProgressHook = null;
408
409 remainingExpirationTime = NoWork;
410 componentUpdateQueue = null;
381 -
382 - if (__DEV__) {
383 - currentHookNameInDev = null;
384 - }
411 + sideEffectTag = 0;
412
413 // These were reset above
414 // didScheduleRenderPhaseUpdate = false;
415 // renderPhaseUpdates = null;
389 - // numberOfReRenders = -1;
416 + // numberOfReRenders = 0;
417
418 invariant(
419 !didRenderTooFewHooks,
@@ -412,10 +439,12 @@ export function bailoutHooks(
439 export function resetHooks(): void {
440 if (__DEV__) {
441 flushHookMismatchWarnings();
415 - exitDisallowedContextReadInDEV();
416 - isInHookUserCodeInDev = false;
442 }
443
444 + // We can assume the previous dispatcher is always this one, since we set it
445 + // at the beginning of the render phase and there's no re-entrancy.
446 + ReactCurrentDispatcher.current = ContextOnlyDispatcher;
447 +
448 // This is used to reset the state of this module when a component throws.
449 // It's also called inside mountIndeterminateComponent if we determine the
450 // component is a module-style component.
@@ -424,11 +453,14 @@ export function resetHooks(): void {
453
454 firstCurrentHook = null;
455 currentHook = null;
456 + nextCurrentHook = null;
457 firstWorkInProgressHook = null;
458 workInProgressHook = null;
459 + nextWorkInProgressHook = null;
460
461 remainingExpirationTime = NoWork;
462 componentUpdateQueue = null;
463 + sideEffectTag = 0;
464
465 if (__DEV__) {
466 currentHookNameInDev = null;
@@ -436,110 +468,95 @@ export function resetHooks(): void {
468
469 didScheduleRenderPhaseUpdate = false;
470 renderPhaseUpdates = null;
439 - numberOfReRenders = -1;
471 + numberOfReRenders = 0;
472 }
473
442 -function createHook(): Hook {
443 - let hook: Hook = __DEV__
444 - ? {
445 - _debugType: ((currentHookNameInDev: any): HookType),
446 - memoizedState: null,
474 +function mountWorkInProgressHook(): Hook {
475 + const hook: Hook = {
476 + memoizedState: null,
477
448 - baseState: null,
449 - queue: null,
450 - baseUpdate: null,
478 + baseState: null,
479 + queue: null,
480 + baseUpdate: null,
481
452 - next: null,
453 - }
454 - : {
455 - memoizedState: null,
456 -
457 - baseState: null,
458 - queue: null,
459 - baseUpdate: null,
460 -
461 - next: null,
462 - };
482 + next: null,
483 + };
484
464 - return hook;
485 + if (__DEV__) {
486 + (hook: any)._debugType = (currentHookNameInDev: any);
487 + if (
488 + currentlyRenderingFiber !== null &&
489 + currentlyRenderingFiber.alternate !== null
490 + ) {
491 + warning(
492 + false,
493 + '%s: Rendered more hooks than during the previous render. This is ' +
494 + 'not currently supported and may lead to unexpected behavior.',
495 + getComponentName(currentlyRenderingFiber.type),
496 + );
497 + }
498 + }
499 + if (workInProgressHook === null) {
500 + // This is the first hook in the list
501 + firstWorkInProgressHook = workInProgressHook = hook;
502 + } else {
503 + // Append to the end of the list
504 + workInProgressHook = workInProgressHook.next = hook;
505 + }
506 + return workInProgressHook;
507 }
508
467 -function cloneHook(hook: Hook): Hook {
468 - let nextHook: Hook = __DEV__
469 - ? {
470 - _debugType: ((currentHookNameInDev: any): HookType),
471 - memoizedState: hook.memoizedState,
472 -
473 - baseState: hook.baseState,
474 - queue: hook.queue,
475 - baseUpdate: hook.baseUpdate,
509 +function updateWorkInProgressHook(): Hook {
510 + // This function is used both for updates and for re-renders triggered by a
511 + // render phase update. It assumes there is either a current hook we can
512 + // clone, or a work-in-progress hook from a previous render pass that we can
513 + // use as a base. When we reach the end of the base list, we must switch to
514 + // the dispatcher used for mounts.
515 + if (nextWorkInProgressHook !== null) {
516 + // There's already a work-in-progress. Reuse it.
517 + workInProgressHook = nextWorkInProgressHook;
518 + nextWorkInProgressHook = workInProgressHook.next;
519 +
520 + currentHook = nextCurrentHook;
521 + nextCurrentHook = currentHook !== null ? currentHook.next : null;
522 + } else {
523 + // Clone from the current hook.
524 + invariant(
525 + nextCurrentHook !== null,
526 + 'Rendered more hooks than during the previous render.',
527 + );
528 + currentHook = nextCurrentHook;
529
477 - next: null,
478 - }
479 - : {
480 - memoizedState: hook.memoizedState,
530 + const newHook: Hook = {
531 + memoizedState: currentHook.memoizedState,
532
482 - baseState: hook.baseState,
483 - queue: hook.queue,
484 - baseUpdate: hook.baseUpdate,
533 + baseState: currentHook.baseState,
534 + queue: currentHook.queue,
535 + baseUpdate: currentHook.baseUpdate,
536
486 - next: null,
487 - };
537 + next: null,
538 + };
539
489 - if (__DEV__) {
490 - if (currentHookMismatchInDev === null) {
491 - if (currentHookNameInDev !== ((hook: any): HookDev)._debugType) {
492 - currentHookMismatchInDev = new Error('tracer').stack
493 - .split('\n')
494 - .slice(4)
495 - .join('\n');
540 + if (__DEV__) {
541 + (newHook: any)._debugType = (currentHookNameInDev: any);
542 + if (currentHookMismatchInDev === null) {
543 + if (currentHookNameInDev !== ((currentHook: any): HookDev)._debugType) {
544 + currentHookMismatchInDev = new Error('tracer').stack
545 + .split('\n')
546 + .slice(4)
547 + .join('\n');
548 + }
549 }
550 }
498 - }
499 - return nextHook;
500 -}
551
502 -function createWorkInProgressHook(): Hook {
503 - if (workInProgressHook === null) {
504 - // This is the first hook in the list
505 - if (firstWorkInProgressHook === null) {
506 - currentHook = firstCurrentHook;
507 - if (currentHook === null) {
508 - // This is a newly mounted hook
509 - workInProgressHook = createHook();
510 - } else {
511 - // Clone the current hook.
512 - workInProgressHook = cloneHook(currentHook);
513 - }
514 - firstWorkInProgressHook = workInProgressHook;
515 - } else {
516 - // There's already a work-in-progress. Reuse it.
517 - currentHook = firstCurrentHook;
518 - workInProgressHook = firstWorkInProgressHook;
519 - }
520 - } else {
521 - if (workInProgressHook.next === null) {
522 - let hook;
523 - if (currentHook === null) {
524 - // This is a newly mounted hook
525 - hook = createHook();
526 - } else {
527 - currentHook = currentHook.next;
528 - if (currentHook === null) {
529 - // This is a newly mounted hook
530 - hook = createHook();
531 - } else {
532 - // Clone the current hook.
533 - hook = cloneHook(currentHook);
534 - }
535 - }
536 - // Append to the end of the list
537 - workInProgressHook = workInProgressHook.next = hook;
552 + if (workInProgressHook === null) {
553 + // This is the first hook in the list.
554 + workInProgressHook = firstWorkInProgressHook = newHook;
555 } else {
539 - // There's already a work-in-progress. Reuse it.
540 - workInProgressHook = workInProgressHook.next;
541 - currentHook = currentHook !== null ? currentHook.next : null;
556 + // Append to the end of the list.
557 + workInProgressHook = workInProgressHook.next = newHook;
558 }
559 + nextCurrentHook = currentHook.next;
560 }
561 return workInProgressHook;
562 }
@@ -554,212 +571,216 @@ function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
571 return typeof action === 'function' ? action(state) : action;
572 }
573
557 -export function useContext<T>(
574 +function mountContext<T>(
575 context: ReactContext<T>,
576 observedBits: void | number | boolean,
577 ): T {
578 if (__DEV__) {
562 - currentHookNameInDev = 'useContext';
563 - createWorkInProgressHook();
564 - currentHookNameInDev = null;
579 + mountWorkInProgressHook();
580 }
566 - // Ensure we're in a function component (class components support only the
567 - // .unstable_read() form)
568 - resolveCurrentlyRenderingFiber();
581 return readContext(context, observedBits);
582 }
583
572 -export function useState<S>(
573 - initialState: (() => S) | S,
574 -): [S, Dispatch<BasicStateAction<S>>] {
584 +function updateContext<T>(
585 + context: ReactContext<T>,
586 + observedBits: void | number | boolean,
587 +): T {
588 if (__DEV__) {
576 - currentHookNameInDev = 'useState';
589 + updateWorkInProgressHook();
590 }
578 - return useReducer(
579 - basicStateReducer,
580 - // useReducer has a special case to support lazy useState initializers
581 - (initialState: any),
582 - );
591 + return readContext(context, observedBits);
592 }
593
585 -export function useReducer<S, A>(
594 +function mountReducer<S, A>(
595 reducer: (S, A) => S,
587 - initialState: S,
588 - initialAction: A | void | null,
596 + initialState: void | S,
597 + initialAction: void | null | A,
598 ): [S, Dispatch<A>] {
590 - if (__DEV__) {
591 - if (reducer !== basicStateReducer) {
592 - currentHookNameInDev = 'useReducer';
593 - }
594 - }
595 - currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
596 - workInProgressHook = createWorkInProgressHook();
597 - if (__DEV__) {
598 - currentHookNameInDev = null;
599 + const hook = mountWorkInProgressHook();
600 + // TODO: Lazy init API will change before release.
601 + if (initialAction !== undefined && initialAction !== null) {
602 + // $FlowFixMe - Must express with overloading.
603 + initialState = reducer(initialState, initialAction);
604 }
600 - let queue: UpdateQueue<S, A> | null = (workInProgressHook.queue: any);
601 - if (queue !== null) {
602 - // Already have a queue, so this is an update.
603 - if (numberOfReRenders > 0) {
604 - // This is a re-render. Apply the new render phase updates to the previous
605 - // work-in-progress hook.
606 - const dispatch: Dispatch<A> = (queue.dispatch: any);
607 - if (renderPhaseUpdates !== null) {
608 - // Render phase updates are stored in a map of queue -> linked list
609 - const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
610 - if (firstRenderPhaseUpdate !== undefined) {
611 - renderPhaseUpdates.delete(queue);
612 - let newState = workInProgressHook.memoizedState;
613 - let update = firstRenderPhaseUpdate;
614 - do {
615 - // Process this render phase update. We don't have to check the
616 - // priority because it will always be the same as the current
617 - // render's.
618 - const action = update.action;
619 - if (__DEV__) {
620 - enterDisallowedContextReadInDEV();
621 - isInHookUserCodeInDev = true;
622 - }
623 - newState = reducer(newState, action);
624 - if (__DEV__) {
625 - exitDisallowedContextReadInDEV();
626 - isInHookUserCodeInDev = false;
627 - }
628 - update = update.next;
629 - } while (update !== null);
630 -
631 - workInProgressHook.memoizedState = newState;
632 -
633 - // Don't persist the state accumlated from the render phase updates to
634 - // the base state unless the queue is empty.
635 - // TODO: Not sure if this is the desired semantics, but it's what we
636 - // do for gDSFP. I can't remember why.
637 - if (workInProgressHook.baseUpdate === queue.last) {
638 - workInProgressHook.baseState = newState;
639 - }
605 + hook.memoizedState = hook.baseState = initialState;
606 + const queue = (hook.queue = {
607 + last: null,
608 + dispatch: null,
609 + eagerReducer: reducer,
610 + eagerState: (initialState: any),
611 + });
612 + const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
613 + null,
614 + // Flow doesn't know this is non-null, but we do.
615 + ((currentlyRenderingFiber: any): Fiber),
616 + queue,
617 + ): any));
618 + return [hook.memoizedState, dispatch];
619 +}
620
641 - return [newState, dispatch];
621 +function updateReducer<S, A>(
622 + reducer: (S, A) => S,
623 + initialState: void | S,
624 + initialAction: void | null | A,
625 +): [S, Dispatch<A>] {
626 + const hook = updateWorkInProgressHook();
627 + const queue = hook.queue;
628 + invariant(
629 + queue !== null,
630 + 'Should have a queue. This is likely a bug in React. Please file an issue.',
631 + );
632 +
633 + if (numberOfReRenders > 0) {
634 + // This is a re-render. Apply the new render phase updates to the previous
635 + // work-in-progress hook.
636 + const dispatch: Dispatch<A> = (queue.dispatch: any);
637 + if (renderPhaseUpdates !== null) {
638 + // Render phase updates are stored in a map of queue -> linked list
639 + const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
640 + if (firstRenderPhaseUpdate !== undefined) {
641 + renderPhaseUpdates.delete(queue);
642 + let newState = hook.memoizedState;
643 + let update = firstRenderPhaseUpdate;
644 + do {
645 + // Process this render phase update. We don't have to check the
646 + // priority because it will always be the same as the current
647 + // render's.
648 + const action = update.action;
649 + newState = reducer(newState, action);
650 + update = update.next;
651 + } while (update !== null);
652 +
653 + // Mark that the fiber performed work, but only if the new state is
654 + // different from the current state.
655 + if (newState !== hook.memoizedState) {
656 + markWorkInProgressReceivedUpdate();
657 }
658 +
659 + hook.memoizedState = newState;
660 +
661 + // Don't persist the state accumlated from the render phase updates to
662 + // the base state unless the queue is empty.
663 + // TODO: Not sure if this is the desired semantics, but it's what we
664 + // do for gDSFP. I can't remember why.
665 + if (hook.baseUpdate === queue.last) {
666 + hook.baseState = newState;
667 + }
668 +
669 + return [newState, dispatch];
670 }
644 - return [workInProgressHook.memoizedState, dispatch];
671 }
672 + return [hook.memoizedState, dispatch];
673 + }
674
647 - // The last update in the entire queue
648 - const last = queue.last;
649 - // The last update that is part of the base state.
650 - const baseUpdate = workInProgressHook.baseUpdate;
651 - const baseState = workInProgressHook.baseState;
652 -
653 - // Find the first unprocessed update.
654 - let first;
655 - if (baseUpdate !== null) {
656 - if (last !== null) {
657 - // For the first update, the queue is a circular linked list where
658 - // `queue.last.next = queue.first`. Once the first update commits, and
659 - // the `baseUpdate` is no longer empty, we can unravel the list.
660 - last.next = null;
661 - }
662 - first = baseUpdate.next;
663 - } else {
664 - first = last !== null ? last.next : null;
675 + // The last update in the entire queue
676 + const last = queue.last;
677 + // The last update that is part of the base state.
678 + const baseUpdate = hook.baseUpdate;
679 + const baseState = hook.baseState;
680 +
681 + // Find the first unprocessed update.
682 + let first;
683 + if (baseUpdate !== null) {
684 + if (last !== null) {
685 + // For the first update, the queue is a circular linked list where
686 + // `queue.last.next = queue.first`. Once the first update commits, and
687 + // the `baseUpdate` is no longer empty, we can unravel the list.
688 + last.next = null;
689 }
666 - if (first !== null) {
667 - let newState = baseState;
668 - let newBaseState = null;
669 - let newBaseUpdate = null;
670 - let prevUpdate = baseUpdate;
671 - let update = first;
672 - let didSkip = false;
673 - do {
674 - const updateExpirationTime = update.expirationTime;
675 - if (updateExpirationTime < renderExpirationTime) {
676 - // Priority is insufficient. Skip this update. If this is the first
677 - // skipped update, the previous update/state is the new base
678 - // update/state.
679 - if (!didSkip) {
680 - didSkip = true;
681 - newBaseUpdate = prevUpdate;
682 - newBaseState = newState;
683 - }
684 - // Update the remaining priority in the queue.
685 - if (updateExpirationTime > remainingExpirationTime) {
686 - remainingExpirationTime = updateExpirationTime;
687 - }
690 + first = baseUpdate.next;
691 + } else {
692 + first = last !== null ? last.next : null;
693 + }
694 + if (first !== null) {
695 + let newState = baseState;
696 + let newBaseState = null;
697 + let newBaseUpdate = null;
698 + let prevUpdate = baseUpdate;
699 + let update = first;
700 + let didSkip = false;
701 + do {
702 + const updateExpirationTime = update.expirationTime;
703 + if (updateExpirationTime < renderExpirationTime) {
704 + // Priority is insufficient. Skip this update. If this is the first
705 + // skipped update, the previous update/state is the new base
706 + // update/state.
707 + if (!didSkip) {
708 + didSkip = true;
709 + newBaseUpdate = prevUpdate;
710 + newBaseState = newState;
711 + }
712 + // Update the remaining priority in the queue.
713 + if (updateExpirationTime > remainingExpirationTime) {
714 + remainingExpirationTime = updateExpirationTime;
715 + }
716 + } else {
717 + // Process this update.
718 + if (update.eagerReducer === reducer) {
719 + // If this update was processed eagerly, and its reducer matches the
720 + // current reducer, we can use the eagerly computed state.
721 + newState = ((update.eagerState: any): S);
722 } else {
689 - // Process this update.
690 - if (update.eagerReducer === reducer) {
691 - // If this update was processed eagerly, and its reducer matches the
692 - // current reducer, we can use the eagerly computed state.
693 - newState = ((update.eagerState: any): S);
694 - } else {
695 - const action = update.action;
696 - if (__DEV__) {
697 - enterDisallowedContextReadInDEV();
698 - isInHookUserCodeInDev = true;
699 - }
700 - newState = reducer(newState, action);
701 - if (__DEV__) {
702 - exitDisallowedContextReadInDEV();
703 - isInHookUserCodeInDev = false;
704 - }
705 - }
723 + const action = update.action;
724 + newState = reducer(newState, action);
725 }
707 - prevUpdate = update;
708 - update = update.next;
709 - } while (update !== null && update !== first);
710 -
711 - if (!didSkip) {
712 - newBaseUpdate = prevUpdate;
713 - newBaseState = newState;
726 }
727 + prevUpdate = update;
728 + update = update.next;
729 + } while (update !== null && update !== first);
730
716 - workInProgressHook.memoizedState = newState;
717 - workInProgressHook.baseUpdate = newBaseUpdate;
718 - workInProgressHook.baseState = newBaseState;
719 -
720 - // Mark that the fiber performed work, but only if the new state is
721 - // different from the current state.
722 - if (newState !== (currentHook: any).memoizedState) {
723 - markWorkInProgressReceivedUpdate();
724 - }
725 -
726 - queue.eagerReducer = reducer;
727 - queue.eagerState = newState;
731 + if (!didSkip) {
732 + newBaseUpdate = prevUpdate;
733 + newBaseState = newState;
734 }
735
730 - const dispatch: Dispatch<A> = (queue.dispatch: any);
731 - return [workInProgressHook.memoizedState, dispatch];
732 - }
733 - if (__DEV__) {
734 - enterDisallowedContextReadInDEV();
735 - isInHookUserCodeInDev = true;
736 - }
737 - // There's no existing queue, so this is the initial render.
738 - if (reducer === basicStateReducer) {
739 - // Special case for `useState`.
740 - if (typeof initialState === 'function') {
741 - initialState = initialState();
736 + // Mark that the fiber performed work, but only if the new state is
737 + // different from the current state.
738 + if (newState !== hook.memoizedState) {
739 + markWorkInProgressReceivedUpdate();
740 }
743 - } else if (initialAction !== undefined && initialAction !== null) {
744 - initialState = reducer(initialState, initialAction);
741 +
742 + hook.memoizedState = newState;
743 + hook.baseUpdate = newBaseUpdate;
744 + hook.baseState = newBaseState;
745 +
746 + queue.eagerReducer = reducer;
747 + queue.eagerState = newState;
748 }
746 - if (__DEV__) {
747 - exitDisallowedContextReadInDEV();
748 - isInHookUserCodeInDev = false;
749 +
750 + const dispatch: Dispatch<A> = (queue.dispatch: any);
751 + return [hook.memoizedState, dispatch];
752 +}
753 +
754 +function mountState<S>(
755 + initialState: (() => S) | S,
756 +): [S, Dispatch<BasicStateAction<S>>] {
757 + const hook = mountWorkInProgressHook();
758 + // TODO: Lazy init API will change before release.
759 + if (typeof initialState === 'function') {
760 + initialState = initialState();
761 }
750 - workInProgressHook.memoizedState = workInProgressHook.baseState = initialState;
751 - queue = workInProgressHook.queue = {
762 + hook.memoizedState = hook.baseState = initialState;
763 + const queue = (hook.queue = {
764 last: null,
765 dispatch: null,
754 - eagerReducer: reducer,
755 - eagerState: initialState,
756 - };
757 - const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
766 + eagerReducer: basicStateReducer,
767 + eagerState: (initialState: any),
768 + });
769 + const dispatch: Dispatch<
770 + BasicStateAction<S>,
771 + > = (queue.dispatch = (dispatchAction.bind(
772 null,
759 - currentlyRenderingFiber,
773 + // Flow doesn't know this is non-null, but we do.
774 + ((currentlyRenderingFiber: any): Fiber),
775 queue,
776 ): any));
762 - return [workInProgressHook.memoizedState, dispatch];
777 + return [hook.memoizedState, dispatch];
778 +}
779 +
780 +function updateState<S>(
781 + initialState: (() => S) | S,
782 +): [S, Dispatch<BasicStateAction<S>>] {
783 + return updateReducer(basicStateReducer, (initialState: any));
784 }
785
786 function pushEffect(tag, create, destroy, deps) {
@@ -788,62 +809,33 @@ function pushEffect(tag, create, destroy, deps) {
809 return effect;
810 }
811
791 -export function useRef<T>(initialValue: T): {current: T} {
792 - currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
812 +function mountRef<T>(initialValue: T): {current: T} {
813 + const hook = mountWorkInProgressHook();
814 + const ref = {current: initialValue};
815 if (__DEV__) {
794 - currentHookNameInDev = 'useRef';
795 - }
796 - workInProgressHook = createWorkInProgressHook();
797 - if (__DEV__) {
798 - currentHookNameInDev = null;
799 - }
800 - let ref;
801 -
802 - if (workInProgressHook.memoizedState === null) {
803 - ref = {current: initialValue};
804 - if (__DEV__) {
805 - Object.seal(ref);
806 - }
807 - workInProgressHook.memoizedState = ref;
808 - } else {
809 - ref = workInProgressHook.memoizedState;
816 + Object.seal(ref);
817 }
818 + hook.memoizedState = ref;
819 return ref;
820 }
821
814 -export function useLayoutEffect(
815 - create: () => mixed,
816 - deps: Array<mixed> | void | null,
817 -): void {
818 - if (__DEV__) {
819 - if (currentHookNameInDev !== 'useImperativeHandle') {
820 - currentHookNameInDev = 'useLayoutEffect';
821 - }
822 - }
823 - useEffectImpl(UpdateEffect, UnmountMutation | MountLayout, create, deps);
822 +function updateRef<T>(initialValue: T): {current: T} {
823 + const hook = updateWorkInProgressHook();
824 + return hook.memoizedState;
825 }
826
826 -export function useEffect(
827 - create: () => mixed,
828 - deps: Array<mixed> | void | null,
829 -): void {
830 - if (__DEV__) {
831 - currentHookNameInDev = 'useEffect';
832 - }
833 - useEffectImpl(
834 - UpdateEffect | PassiveEffect,
835 - UnmountPassive | MountPassive,
836 - create,
837 - deps,
838 - );
827 +function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
828 + const hook = mountWorkInProgressHook();
829 + const nextDeps = deps === undefined ? null : deps;
830 + sideEffectTag |= fiberEffectTag;
831 + hook.memoizedState = pushEffect(hookEffectTag, create, null, nextDeps);
832 }
833
841 -function useEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
842 - currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
843 - workInProgressHook = createWorkInProgressHook();
844 -
834 +function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
835 + const hook = updateWorkInProgressHook();
836 const nextDeps = deps === undefined ? null : deps;
837 let destroy = null;
838 +
839 if (currentHook !== null) {
840 const prevEffect = currentHook.memoizedState;
841 destroy = prevEffect.destroy;
@@ -851,33 +843,96 @@ function useEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
843 const prevDeps = prevEffect.deps;
844 if (areHookInputsEqual(nextDeps, prevDeps)) {
845 pushEffect(NoHookEffect, create, destroy, nextDeps);
854 - if (__DEV__) {
855 - currentHookNameInDev = null;
856 - }
846 return;
847 }
848 }
849 }
850
862 - currentlyRenderingFiber.effectTag |= fiberEffectTag;
863 - workInProgressHook.memoizedState = pushEffect(
864 - hookEffectTag,
851 + sideEffectTag |= fiberEffectTag;
852 + hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
853 +}
854 +
855 +function mountEffect(
856 + create: () => mixed,
857 + deps: Array<mixed> | void | null,
858 +): void {
859 + return mountEffectImpl(
860 + UpdateEffect | PassiveEffect,
861 + UnmountPassive | MountPassive,
862 create,
866 - destroy,
867 - nextDeps,
863 + deps,
864 );
869 - if (__DEV__) {
870 - currentHookNameInDev = null;
865 +}
866 +
867 +function updateEffect(
868 + create: () => mixed,
869 + deps: Array<mixed> | void | null,
870 +): void {
871 + return updateEffectImpl(
872 + UpdateEffect | PassiveEffect,
873 + UnmountPassive | MountPassive,
874 + create,
875 + deps,
876 + );
877 +}
878 +
879 +function mountLayoutEffect(
880 + create: () => mixed,
881 + deps: Array<mixed> | void | null,
882 +): void {
883 + return mountEffectImpl(
884 + UpdateEffect,
885 + UnmountMutation | MountLayout,
886 + create,
887 + deps,
888 + );
889 +}
890 +
891 +function updateLayoutEffect(
892 + create: () => mixed,
893 + deps: Array<mixed> | void | null,
894 +): void {
895 + return updateEffectImpl(
896 + UpdateEffect,
897 + UnmountMutation | MountLayout,
898 + create,
899 + deps,
900 + );
901 +}
902 +
903 +function imperativeHandleEffect<T>(
904 + create: () => T,
905 + ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
906 +) {
907 + if (typeof ref === 'function') {
908 + const refCallback = ref;
909 + const inst = create();
910 + refCallback(inst);
911 + return () => refCallback(null);
912 + } else if (ref !== null && ref !== undefined) {
913 + const refObject = ref;
914 + if (__DEV__) {
915 + warning(
916 + refObject.hasOwnProperty('current'),
917 + 'Expected useImperativeHandle() first argument to either be a ' +
918 + 'ref callback or React.createRef() object. Instead received: %s.',
919 + 'an object with keys {' + Object.keys(refObject).join(', ') + '}',
920 + );
921 + }
922 + const inst = create();
923 + refObject.current = inst;
924 + return () => {
925 + refObject.current = null;
926 + };
927 }
928 }
929
874 -export function useImperativeHandle<T>(
930 +function mountImperativeHandle<T>(
931 ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
932 create: () => T,
933 deps: Array<mixed> | void | null,
934 ): void {
935 if (__DEV__) {
880 - currentHookNameInDev = 'useImperativeHandle';
936 warning(
937 typeof create === 'function',
938 'Expected useImperativeHandle() second argument to be a function ' +
@@ -885,122 +940,105 @@ export function useImperativeHandle<T>(
940 create !== null ? typeof create : 'null',
941 );
942 }
943 +
944 // TODO: If deps are provided, should we skip comparing the ref itself?
889 - const nextDeps =
945 + const effectDeps =
946 deps !== null && deps !== undefined ? deps.concat([ref]) : [ref];
947
892 - // TODO: I've implemented this on top of useEffect because it's almost the
893 - // same thing, and it would require an equal amount of code. It doesn't seem
894 - // like a common enough use case to justify the additional size.
895 - useLayoutEffect(() => {
896 - if (typeof ref === 'function') {
897 - const refCallback = ref;
898 - const inst = create();
899 - refCallback(inst);
900 - return () => refCallback(null);
901 - } else if (ref !== null && ref !== undefined) {
902 - const refObject = ref;
903 - if (__DEV__) {
904 - warning(
905 - refObject.hasOwnProperty('current'),
906 - 'Expected useImperativeHandle() first argument to either be a ' +
907 - 'ref callback or React.createRef() object. Instead received: %s.',
908 - 'an object with keys {' + Object.keys(refObject).join(', ') + '}',
909 - );
910 - }
911 - const inst = create();
912 - refObject.current = inst;
913 - return () => {
914 - refObject.current = null;
915 - };
916 - }
917 - }, nextDeps);
948 + return mountEffectImpl(
949 + UpdateEffect,
950 + UnmountMutation | MountLayout,
951 + imperativeHandleEffect.bind(null, create, ref),
952 + effectDeps,
953 + );
954 }
955
920 -export function useDebugValue(
921 - value: any,
922 - formatterFn: ?(value: any) => any,
956 +function updateImperativeHandle<T>(
957 + ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
958 + create: () => T,
959 + deps: Array<mixed> | void | null,
960 ): void {
961 if (__DEV__) {
925 - currentHookNameInDev = 'useDebugValue';
962 + warning(
963 + typeof create === 'function',
964 + 'Expected useImperativeHandle() second argument to be a function ' +
965 + 'that creates a handle. Instead received: %s.',
966 + create !== null ? typeof create : 'null',
967 + );
968 }
969
928 - // This will trigger a warning if the hook is used in a non-Function component.
929 - resolveCurrentlyRenderingFiber();
970 + // TODO: If deps are provided, should we skip comparing the ref itself?
971 + const effectDeps =
972 + deps !== null && deps !== undefined ? deps.concat([ref]) : [ref];
973 +
974 + return updateEffectImpl(
975 + UpdateEffect,
976 + UnmountMutation | MountLayout,
977 + imperativeHandleEffect.bind(null, create, ref),
978 + effectDeps,
979 + );
980 +}
981
982 +function mountDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
983 // This hook is normally a no-op.
984 // The react-debug-hooks package injects its own implementation
985 // so that e.g. DevTools can display custom hook values.
986 }
987
936 -export function useCallback<T>(
937 - callback: T,
938 - deps: Array<mixed> | void | null,
939 -): T {
940 - if (__DEV__) {
941 - currentHookNameInDev = 'useCallback';
942 - }
943 - currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
944 - workInProgressHook = createWorkInProgressHook();
988 +const updateDebugValue = mountDebugValue;
989
990 +function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
991 + const hook = mountWorkInProgressHook();
992 const nextDeps = deps === undefined ? null : deps;
993 + hook.memoizedState = [callback, nextDeps];
994 + return callback;
995 +}
996
948 - const prevState = workInProgressHook.memoizedState;
997 +function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
998 + const hook = updateWorkInProgressHook();
999 + const nextDeps = deps === undefined ? null : deps;
1000 + const prevState = hook.memoizedState;
1001 if (prevState !== null) {
1002 if (nextDeps !== null) {
1003 const prevDeps: Array<mixed> | null = prevState[1];
1004 if (areHookInputsEqual(nextDeps, prevDeps)) {
953 - currentHookNameInDev = null;
1005 return prevState[0];
1006 }
1007 }
1008 }
958 - workInProgressHook.memoizedState = [callback, nextDeps];
959 - if (__DEV__) {
960 - currentHookNameInDev = null;
961 - }
1009 + hook.memoizedState = [callback, nextDeps];
1010 return callback;
1011 }
1012
965 -export function useMemo<T>(
1013 +function mountMemo<T>(
1014 nextCreate: () => T,
1015 deps: Array<mixed> | void | null,
1016 ): T {
969 - if (__DEV__) {
970 - currentHookNameInDev = 'useMemo';
971 - }
972 - currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
973 - workInProgressHook = createWorkInProgressHook();
974 -
1017 + const hook = mountWorkInProgressHook();
1018 const nextDeps = deps === undefined ? null : deps;
1019 + const nextValue = nextCreate();
1020 + hook.memoizedState = [nextValue, nextDeps];
1021 + return nextValue;
1022 +}
1023
977 - const prevState = workInProgressHook.memoizedState;
1024 +function updateMemo<T>(
1025 + nextCreate: () => T,
1026 + deps: Array<mixed> | void | null,
1027 +): T {
1028 + const hook = updateWorkInProgressHook();
1029 + const nextDeps = deps === undefined ? null : deps;
1030 + const prevState = hook.memoizedState;
1031 if (prevState !== null) {
1032 // Assume these are defined. If they're not, areHookInputsEqual will warn.
1033 if (nextDeps !== null) {
1034 const prevDeps: Array<mixed> | null = prevState[1];
1035 if (areHookInputsEqual(nextDeps, prevDeps)) {
983 - if (__DEV__) {
984 - currentHookNameInDev = null;
985 - }
1036 return prevState[0];
1037 }
1038 }
1039 }
990 -
991 - if (__DEV__) {
992 - enterDisallowedContextReadInDEV();
993 - isInHookUserCodeInDev = true;
994 - }
1040 const nextValue = nextCreate();
996 - if (__DEV__) {
997 - exitDisallowedContextReadInDEV();
998 - isInHookUserCodeInDev = false;
999 - }
1000 - workInProgressHook.memoizedState = [nextValue, nextDeps];
1001 - if (__DEV__) {
1002 - currentHookNameInDev = null;
1003 - }
1041 + hook.memoizedState = [nextValue, nextDeps];
1042 return nextValue;
1043 }
1044
@@ -1092,17 +1130,14 @@ function dispatchAction<S, A>(
1130 // same as the current state, we may be able to bail out entirely.
1131 const eagerReducer = queue.eagerReducer;
1132 if (eagerReducer !== null) {
1133 + let prevDispatcher;
1134 + if (__DEV__) {
1135 + prevDispatcher = ReactCurrentDispatcher.current;
1136 + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
1137 + }
1138 try {
1139 const currentState: S = (queue.eagerState: any);
1097 - if (__DEV__) {
1098 - enterDisallowedContextReadInDEV();
1099 - isInHookUserCodeInDev = true;
1100 - }
1140 const eagerState = eagerReducer(currentState, action);
1102 - if (__DEV__) {
1103 - exitDisallowedContextReadInDEV();
1104 - isInHookUserCodeInDev = false;
1105 - }
1141 // Stash the eagerly computed state, and the reducer used to compute
1142 // it, on the update object. If the reducer hasn't changed by the
1143 // time we enter the render phase, then the eager state can be used
@@ -1120,8 +1155,7 @@ function dispatchAction<S, A>(
1155 // Suppress the error. It will throw again in the render phase.
1156 } finally {
1157 if (__DEV__) {
1123 - exitDisallowedContextReadInDEV();
1124 - isInHookUserCodeInDev = false;
1158 + ReactCurrentDispatcher.current = prevDispatcher;
1159 }
1160 }
1161 }
@@ -1129,3 +1163,432 @@ function dispatchAction<S, A>(
1163 scheduleWork(fiber, expirationTime);
1164 }
1165 }
1166 +
1167 +export const ContextOnlyDispatcher: Dispatcher = {
1168 + readContext,
1169 +
1170 + useCallback: throwInvalidHookError,
1171 + useContext: throwInvalidHookError,
1172 + useEffect: throwInvalidHookError,
1173 + useImperativeHandle: throwInvalidHookError,
1174 + useLayoutEffect: throwInvalidHookError,
1175 + useMemo: throwInvalidHookError,
1176 + useReducer: throwInvalidHookError,
1177 + useRef: throwInvalidHookError,
1178 + useState: throwInvalidHookError,
1179 + useDebugValue: throwInvalidHookError,
1180 +};
1181 +
1182 +const HooksDispatcherOnMount: Dispatcher = {
1183 + readContext,
1184 +
1185 + useCallback: mountCallback,
1186 + useContext: readContext,
1187 + useEffect: mountEffect,
1188 + useImperativeHandle: mountImperativeHandle,
1189 + useLayoutEffect: mountLayoutEffect,
1190 + useMemo: mountMemo,
1191 + useReducer: mountReducer,
1192 + useRef: mountRef,
1193 + useState: mountState,
1194 + useDebugValue: mountDebugValue,
1195 +};
1196 +
1197 +const HooksDispatcherOnUpdate: Dispatcher = {
1198 + readContext,
1199 +
1200 + useCallback: updateCallback,
1201 + useContext: readContext,
1202 + useEffect: updateEffect,
1203 + useImperativeHandle: updateImperativeHandle,
1204 + useLayoutEffect: updateLayoutEffect,
1205 + useMemo: updateMemo,
1206 + useReducer: updateReducer,
1207 + useRef: updateRef,
1208 + useState: updateState,
1209 + useDebugValue: updateDebugValue,
1210 +};
1211 +
1212 +let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
1213 +let HooksDispatcherOnUpdateInDEV: Dispatcher | null = null;
1214 +let InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher | null = null;
1215 +let InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher | null = null;
1216 +
1217 +if (__DEV__) {
1218 + const warnInvalidContextAccess = () => {
1219 + warning(
1220 + false,
1221 + 'Context can only be read while React is rendering. ' +
1222 + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1223 + 'In function components, you can read it directly in the function body, but not ' +
1224 + 'inside Hooks like useReducer() or useMemo().',
1225 + );
1226 + };
1227 +
1228 + const warnInvalidHookAccess = () => {
1229 + warning(
1230 + false,
1231 + 'Hooks can only be called inside the body of a function component. ' +
1232 + 'Do not call Hooks inside other Hooks. For more information, see ' +
1233 + 'https://fb.me/rules-of-hooks',
1234 + );
1235 + };
1236 +
1237 + HooksDispatcherOnMountInDEV = {
1238 + readContext<T>(
1239 + context: ReactContext<T>,
1240 + observedBits: void | number | boolean,
1241 + ): T {
1242 + return readContext(context, observedBits);
1243 + },
1244 +
1245 + useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
1246 + currentHookNameInDev = 'useCallback';
1247 + return mountCallback(callback, deps);
1248 + },
1249 + useContext<T>(
1250 + context: ReactContext<T>,
1251 + observedBits: void | number | boolean,
1252 + ): T {
1253 + currentHookNameInDev = 'useContext';
1254 + return mountContext(context, observedBits);
1255 + },
1256 + useEffect(create: () => mixed, deps: Array<mixed> | void | null): void {
1257 + currentHookNameInDev = 'useEffect';
1258 + return mountEffect(create, deps);
1259 + },
1260 + useImperativeHandle<T>(
1261 + ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
1262 + create: () => T,
1263 + deps: Array<mixed> | void | null,
1264 + ): void {
1265 + currentHookNameInDev = 'useImperativeHandle';
1266 + return mountImperativeHandle(ref, create, deps);
1267 + },
1268 + useLayoutEffect(
1269 + create: () => mixed,
1270 + deps: Array<mixed> | void | null,
1271 + ): void {
1272 + currentHookNameInDev = 'useLayoutEffect';
1273 + return mountLayoutEffect(create, deps);
1274 + },
1275 + useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
1276 + currentHookNameInDev = 'useMemo';
1277 + const prevDispatcher = ReactCurrentDispatcher.current;
1278 + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1279 + try {
1280 + return mountMemo(create, deps);
1281 + } finally {
1282 + ReactCurrentDispatcher.current = prevDispatcher;
1283 + }
1284 + },
1285 + useReducer<S, A>(
1286 + reducer: (S, A) => S,
1287 + initialState: S,
1288 + initialAction: A | void | null,
1289 + ): [S, Dispatch<A>] {
1290 + currentHookNameInDev = 'useReducer';
1291 + const prevDispatcher = ReactCurrentDispatcher.current;
1292 + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1293 + try {
1294 + return mountReducer(reducer, initialState, initialAction);
1295 + } finally {
1296 + ReactCurrentDispatcher.current = prevDispatcher;
1297 + }
1298 + },
1299 + useRef<T>(initialValue: T): {current: T} {
1300 + currentHookNameInDev = 'useRef';
1301 + return mountRef(initialValue);
1302 + },
1303 + useState<S>(
1304 + initialState: (() => S) | S,
1305 + ): [S, Dispatch<BasicStateAction<S>>] {
1306 + currentHookNameInDev = 'useState';
1307 + const prevDispatcher = ReactCurrentDispatcher.current;
1308 + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1309 + try {
1310 + return mountState(initialState);
1311 + } finally {
1312 + ReactCurrentDispatcher.current = prevDispatcher;
1313 + }
1314 + },
1315 + useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
1316 + currentHookNameInDev = 'useDebugValue';
1317 + return mountDebugValue(value, formatterFn);
1318 + },
1319 + };
1320 +
1321 + HooksDispatcherOnUpdateInDEV = {
1322 + readContext<T>(
1323 + context: ReactContext<T>,
1324 + observedBits: void | number | boolean,
1325 + ): T {
1326 + return readContext(context, observedBits);
1327 + },
1328 +
1329 + useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
1330 + currentHookNameInDev = 'useCallback';
1331 + return updateCallback(callback, deps);
1332 + },
1333 + useContext<T>(
1334 + context: ReactContext<T>,
1335 + observedBits: void | number | boolean,
1336 + ): T {
1337 + currentHookNameInDev = 'useContext';
1338 + return updateContext(context, observedBits);
1339 + },
1340 + useEffect(create: () => mixed, deps: Array<mixed> | void | null): void {
1341 + currentHookNameInDev = 'useEffect';
1342 + return updateEffect(create, deps);
1343 + },
1344 + useImperativeHandle<T>(
1345 + ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
1346 + create: () => T,
1347 + deps: Array<mixed> | void | null,
1348 + ): void {
1349 + currentHookNameInDev = 'useImperativeHandle';
1350 + return updateImperativeHandle(ref, create, deps);
1351 + },
1352 + useLayoutEffect(
1353 + create: () => mixed,
1354 + deps: Array<mixed> | void | null,
1355 + ): void {
1356 + currentHookNameInDev = 'useLayoutEffect';
1357 + return updateLayoutEffect(create, deps);
1358 + },
1359 + useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
1360 + currentHookNameInDev = 'useMemo';
1361 + const prevDispatcher = ReactCurrentDispatcher.current;
1362 + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
1363 + try {
1364 + return updateMemo(create, deps);
1365 + } finally {
1366 + ReactCurrentDispatcher.current = prevDispatcher;
1367 + }
1368 + },
1369 + useReducer<S, A>(
1370 + reducer: (S, A) => S,
1371 + initialState: S,
1372 + initialAction: A | void | null,
1373 + ): [S, Dispatch<A>] {
1374 + currentHookNameInDev = 'useReducer';
1375 + const prevDispatcher = ReactCurrentDispatcher.current;
1376 + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
1377 + try {
1378 + return updateReducer(reducer, initialState, initialAction);
1379 + } finally {
1380 + ReactCurrentDispatcher.current = prevDispatcher;
1381 + }
1382 + },
1383 + useRef<T>(initialValue: T): {current: T} {
1384 + currentHookNameInDev = 'useRef';
1385 + return updateRef(initialValue);
1386 + },
1387 + useState<S>(
1388 + initialState: (() => S) | S,
1389 + ): [S, Dispatch<BasicStateAction<S>>] {
1390 + currentHookNameInDev = 'useState';
1391 + const prevDispatcher = ReactCurrentDispatcher.current;
1392 + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
1393 + try {
1394 + return updateState(initialState);
1395 + } finally {
1396 + ReactCurrentDispatcher.current = prevDispatcher;
1397 + }
1398 + },
1399 + useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
1400 + currentHookNameInDev = 'useDebugValue';
1401 + return updateDebugValue(value, formatterFn);
1402 + },
1403 + };
1404 +
1405 + InvalidNestedHooksDispatcherOnMountInDEV = {
1406 + readContext<T>(
1407 + context: ReactContext<T>,
1408 + observedBits: void | number | boolean,
1409 + ): T {
1410 + warnInvalidContextAccess();
1411 + return readContext(context, observedBits);
1412 + },
1413 +
1414 + useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
1415 + currentHookNameInDev = 'useCallback';
1416 + warnInvalidHookAccess();
1417 + return mountCallback(callback, deps);
1418 + },
1419 + useContext<T>(
1420 + context: ReactContext<T>,
1421 + observedBits: void | number | boolean,
1422 + ): T {
1423 + currentHookNameInDev = 'useContext';
1424 + warnInvalidHookAccess();
1425 + return mountContext(context, observedBits);
1426 + },
1427 + useEffect(create: () => mixed, deps: Array<mixed> | void | null): void {
1428 + currentHookNameInDev = 'useEffect';
1429 + warnInvalidHookAccess();
1430 + return mountEffect(create, deps);
1431 + },
1432 + useImperativeHandle<T>(
1433 + ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
1434 + create: () => T,
1435 + deps: Array<mixed> | void | null,
1436 + ): void {
1437 + currentHookNameInDev = 'useImperativeHandle';
1438 + warnInvalidHookAccess();
1439 + return mountImperativeHandle(ref, create, deps);
1440 + },
1441 + useLayoutEffect(
1442 + create: () => mixed,
1443 + deps: Array<mixed> | void | null,
1444 + ): void {
1445 + currentHookNameInDev = 'useLayoutEffect';
1446 + warnInvalidHookAccess();
1447 + return mountLayoutEffect(create, deps);
1448 + },
1449 + useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
1450 + currentHookNameInDev = 'useMemo';
1451 + warnInvalidHookAccess();
1452 + const prevDispatcher = ReactCurrentDispatcher.current;
1453 + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1454 + try {
1455 + return mountMemo(create, deps);
1456 + } finally {
1457 + ReactCurrentDispatcher.current = prevDispatcher;
1458 + }
1459 + },
1460 + useReducer<S, A>(
1461 + reducer: (S, A) => S,
1462 + initialState: S,
1463 + initialAction: A | void | null,
1464 + ): [S, Dispatch<A>] {
1465 + currentHookNameInDev = 'useReducer';
1466 + warnInvalidHookAccess();
1467 + const prevDispatcher = ReactCurrentDispatcher.current;
1468 + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1469 + try {
1470 + return mountReducer(reducer, initialState, initialAction);
1471 + } finally {
1472 + ReactCurrentDispatcher.current = prevDispatcher;
1473 + }
1474 + },
1475 + useRef<T>(initialValue: T): {current: T} {
1476 + currentHookNameInDev = 'useRef';
1477 + warnInvalidHookAccess();
1478 + return mountRef(initialValue);
1479 + },
1480 + useState<S>(
1481 + initialState: (() => S) | S,
1482 + ): [S, Dispatch<BasicStateAction<S>>] {
1483 + currentHookNameInDev = 'useState';
1484 + warnInvalidHookAccess();
1485 + const prevDispatcher = ReactCurrentDispatcher.current;
1486 + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1487 + try {
1488 + return mountState(initialState);
1489 + } finally {
1490 + ReactCurrentDispatcher.current = prevDispatcher;
1491 + }
1492 + },
1493 + useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
1494 + currentHookNameInDev = 'useDebugValue';
1495 + warnInvalidHookAccess();
1496 + return mountDebugValue(value, formatterFn);
1497 + },
1498 + };
1499 +
1500 + InvalidNestedHooksDispatcherOnUpdateInDEV = {
1501 + readContext<T>(
1502 + context: ReactContext<T>,
1503 + observedBits: void | number | boolean,
1504 + ): T {
1505 + warnInvalidContextAccess();
1506 + return readContext(context, observedBits);
1507 + },
1508 +
1509 + useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
1510 + currentHookNameInDev = 'useCallback';
1511 + warnInvalidHookAccess();
1512 + return updateCallback(callback, deps);
1513 + },
1514 + useContext<T>(
1515 + context: ReactContext<T>,
1516 + observedBits: void | number | boolean,
1517 + ): T {
1518 + currentHookNameInDev = 'useContext';
1519 + warnInvalidHookAccess();
1520 + return updateContext(context, observedBits);
1521 + },
1522 + useEffect(create: () => mixed, deps: Array<mixed> | void | null): void {
1523 + currentHookNameInDev = 'useEffect';
1524 + warnInvalidHookAccess();
1525 + return updateEffect(create, deps);
1526 + },
1527 + useImperativeHandle<T>(
1528 + ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
1529 + create: () => T,
1530 + deps: Array<mixed> | void | null,
1531 + ): void {
1532 + currentHookNameInDev = 'useImperativeHandle';
1533 + warnInvalidHookAccess();
1534 + return updateImperativeHandle(ref, create, deps);
1535 + },
1536 + useLayoutEffect(
1537 + create: () => mixed,
1538 + deps: Array<mixed> | void | null,
1539 + ): void {
1540 + currentHookNameInDev = 'useLayoutEffect';
1541 + warnInvalidHookAccess();
1542 + return updateLayoutEffect(create, deps);
1543 + },
1544 + useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
1545 + currentHookNameInDev = 'useMemo';
1546 + warnInvalidHookAccess();
1547 + const prevDispatcher = ReactCurrentDispatcher.current;
1548 + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
1549 + try {
1550 + return updateMemo(create, deps);
1551 + } finally {
1552 + ReactCurrentDispatcher.current = prevDispatcher;
1553 + }
1554 + },
1555 + useReducer<S, A>(
1556 + reducer: (S, A) => S,
1557 + initialState: S,
1558 + initialAction: A | void | null,
1559 + ): [S, Dispatch<A>] {
1560 + currentHookNameInDev = 'useReducer';
1561 + warnInvalidHookAccess();
1562 + const prevDispatcher = ReactCurrentDispatcher.current;
1563 + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
1564 + try {
1565 + return updateReducer(reducer, initialState, initialAction);
1566 + } finally {
1567 + ReactCurrentDispatcher.current = prevDispatcher;
1568 + }
1569 + },
1570 + useRef<T>(initialValue: T): {current: T} {
1571 + currentHookNameInDev = 'useRef';
1572 + warnInvalidHookAccess();
1573 + return updateRef(initialValue);
1574 + },
1575 + useState<S>(
1576 + initialState: (() => S) | S,
1577 + ): [S, Dispatch<BasicStateAction<S>>] {
1578 + currentHookNameInDev = 'useState';
1579 + warnInvalidHookAccess();
1580 + const prevDispatcher = ReactCurrentDispatcher.current;
1581 + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
1582 + try {
1583 + return updateState(initialState);
1584 + } finally {
1585 + ReactCurrentDispatcher.current = prevDispatcher;
1586 + }
1587 + },
1588 + useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
1589 + currentHookNameInDev = 'useDebugValue';
1590 + warnInvalidHookAccess();
1591 + return updateDebugValue(value, formatterFn);
1592 + },
1593 + };
1594 +}
packages/react-reconciler/src/ReactFiberScheduler.js
+4 -3
@@ -164,7 +164,7 @@ import {
164 commitDetachRef,
165 commitPassiveHookEffects,
166 } from './ReactFiberCommitWork';
167 -import {Dispatcher} from './ReactFiberDispatcher';
167 +import {ContextOnlyDispatcher} from './ReactFiberHooks';
168
169 export type Thenable = {
170 then(resolve: () => mixed, reject?: () => mixed): mixed,
@@ -1216,7 +1216,8 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void {
1216 flushPassiveEffects();
1217
1218 isWorking = true;
1219 - ReactCurrentDispatcher.current = Dispatcher;
1219 + const previousDispatcher = ReactCurrentDispatcher.current;
1220 + ReactCurrentDispatcher.current = ContextOnlyDispatcher;
1221
1222 const expirationTime = root.nextExpirationTimeToWorkOn;
1223
@@ -1377,7 +1378,7 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void {
1378
1379 // We're done performing work. Time to clean up.
1380 isWorking = false;
1380 - ReactCurrentDispatcher.current = null;
1381 + ReactCurrentDispatcher.current = previousDispatcher;
1382 resetContextDependences();
1383 resetHooks();
1384
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+17 -6
@@ -719,7 +719,7 @@ describe('ReactHooks', () => {
719 const root = ReactTestRenderer.create(<App />);
720 expect(() => root.update(<App />)).toThrow(
721 // The exact message doesn't matter, just make sure we don't allow this
722 - "Cannot read property 'readContext' of null",
722 + 'Context can only be read while React is rendering',
723 );
724 });
725
@@ -740,7 +740,7 @@ describe('ReactHooks', () => {
740
741 expect(() => ReactTestRenderer.create(<App />)).toThrow(
742 // The exact message doesn't matter, just make sure we don't allow this
743 - "Cannot read property 'readContext' of null",
743 + 'Context can only be read while React is rendering',
744 );
745 });
746
@@ -803,7 +803,10 @@ describe('ReactHooks', () => {
803 });
804
805 it('warns when calling hooks inside useReducer', () => {
806 - const {useReducer, useRef} = React;
806 + const {useReducer, useState, useRef} = React;
807 +
808 + spyOnDev(console, 'error');
809 +
810 function App() {
811 const [value, dispatch] = useReducer((state, action) => {
812 useRef(0);
@@ -812,11 +815,19 @@ describe('ReactHooks', () => {
815 if (value === 0) {
816 dispatch('foo');
817 }
818 + useState();
819 return value;
820 }
817 - expect(() => ReactTestRenderer.create(<App />)).toWarnDev(
818 - 'Hooks can only be called inside the body of a function component',
819 - );
821 + expect(() => {
822 + ReactTestRenderer.create(<App />);
823 + }).toThrow('Rendered more hooks than during the previous render.');
824 +
825 + if (__DEV__) {
826 + expect(console.error).toHaveBeenCalledTimes(3);
827 + expect(console.error.calls.argsFor(0)[0]).toContain(
828 + 'Hooks can only be called inside the body of a function component',
829 + );
830 + }
831 });
832
833 it("throws when calling hooks inside useState's initialize function", () => {
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+20 -22
@@ -1596,11 +1596,11 @@ describe('ReactHooksWithNoopRenderer', () => {
1596 });
1597 });
1598
1599 - describe('progressive enhancement', () => {
1599 + describe('progressive enhancement (not supported)', () => {
1600 it('mount additional state', () => {
1601 let updateA;
1602 let updateB;
1603 - let updateC;
1603 + // let updateC;
1604
1605 function App(props) {
1606 const [A, _updateA] = useState(0);
@@ -1610,9 +1610,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1610
1611 let C;
1612 if (props.loadC) {
1613 - const [_C, _updateC] = useState(0);
1614 - C = _C;
1615 - updateC = _updateC;
1613 + useState(0);
1614 } else {
1615 C = '[not loaded]';
1616 }
@@ -1636,14 +1634,14 @@ describe('ReactHooksWithNoopRenderer', () => {
1634 ReactNoop.render(<App loadC={true} />);
1635 expect(() => {
1636 expect(ReactNoop.flush()).toEqual(['A: 2, B: 3, C: 0']);
1639 - }).toWarnDev([
1640 - 'App: Rendered more hooks than during the previous render',
1641 - ]);
1642 - expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 0')]);
1637 + }).toThrow('Rendered more hooks than during the previous render');
1638
1644 - updateC(4);
1645 - expect(ReactNoop.flush()).toEqual(['A: 2, B: 3, C: 4']);
1646 - expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 4')]);
1639 + // Uncomment if/when we support this again
1640 + // expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 0')]);
1641 +
1642 + // updateC(4);
1643 + // expect(ReactNoop.flush()).toEqual(['A: 2, B: 3, C: 4']);
1644 + // expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 4')]);
1645 });
1646
1647 it('unmount state', () => {
@@ -1714,17 +1712,17 @@ describe('ReactHooksWithNoopRenderer', () => {
1712 ReactNoop.render(<App showMore={true} />);
1713 expect(() => {
1714 expect(ReactNoop.flush()).toEqual([]);
1717 - }).toWarnDev([
1718 - 'App: Rendered more hooks than during the previous render',
1719 - ]);
1720 - flushPassiveEffects();
1721 - expect(ReactNoop.clearYields()).toEqual(['Mount B']);
1715 + }).toThrow('Rendered more hooks than during the previous render');
1716
1723 - ReactNoop.render(<App showMore={false} />);
1724 - expect(() => ReactNoop.flush()).toThrow(
1725 - 'Rendered fewer hooks than expected. This may be caused by an ' +
1726 - 'accidental early return statement.',
1727 - );
1717 + // Uncomment if/when we support this again
1718 + // flushPassiveEffects();
1719 + // expect(ReactNoop.clearYields()).toEqual(['Mount B']);
1720 +
1721 + // ReactNoop.render(<App showMore={false} />);
1722 + // expect(() => ReactNoop.flush()).toThrow(
1723 + // 'Rendered fewer hooks than expected. This may be caused by an ' +
1724 + // 'accidental early return statement.',
1725 + // );
1726 });
1727 });
1728 });
packages/react-test-renderer/src/ReactShallowRenderer.js
+1 -1
@@ -18,7 +18,7 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
18 import warning from 'shared/warning';
19 import is from 'shared/objectIs';
20
21 -import typeof {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberDispatcher';
21 +import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberHooks';
22 import type {ReactContext} from 'shared/ReactTypes';
23 import type {ReactElement} from 'shared/ReactElementType';
24
packages/react/src/ReactCurrentDispatcher.js
+1 -1
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import typeof {Dispatcher} from 'react-reconciler/src/ReactFiberDispatcher';
10 +import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';
11
12 /**
13 * Keeps track of the current dispatcher.