@samitouri / QOS-React-2 / commits / 4186952a6f

Fixed incompatibility between react-debug-tools and useContext() (#14940)

* Refactor hook ordering check to use DEV-only data structure. This enables us to warn about more cases (e.g. useContext, useDebugValue) withou the need to add any overhead to production bundles.

Brian Vaughn committed Feb 26, 2019 at 14:24 UTC 4186952a6f3558eb4fae9f6c5f669bd898dc1d97
5 files changed +600 -171
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+38
@@ -427,4 +427,42 @@ describe('ReactHooksInspectionIntegration', () => {
427 expect(setterCalls[0]).not.toBe(initial);
428 expect(setterCalls[1]).toBe(initial);
429 });
430 +
431 + // This test case is based on an open source bug report:
432 + // facebookincubator/redux-react-hook/issues/34#issuecomment-466693787
433 + it('should properly advance the current hook for useContext', () => {
434 + const MyContext = React.createContext(1);
435 +
436 + let incrementCount;
437 +
438 + function Foo(props) {
439 + const context = React.useContext(MyContext);
440 + const [data, setData] = React.useState({count: context});
441 +
442 + incrementCount = () => setData(({count}) => ({count: count + 1}));
443 +
444 + return <div>count: {data.count}</div>;
445 + }
446 +
447 + const renderer = ReactTestRenderer.create(<Foo />);
448 + expect(renderer.toJSON()).toEqual({
449 + type: 'div',
450 + props: {},
451 + children: ['count: ', '1'],
452 + });
453 +
454 + act(incrementCount);
455 + expect(renderer.toJSON()).toEqual({
456 + type: 'div',
457 + props: {},
458 + children: ['count: ', '2'],
459 + });
460 +
461 + const childFiber = renderer.root._currentFiber();
462 + const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
463 + expect(tree).toEqual([
464 + {name: 'Context', value: 1, subHooks: []},
465 + {name: 'State', value: {count: 2}, subHooks: []},
466 + ]);
467 + });
468 });
packages/react-reconciler/src/ReactFiber.js
+7
@@ -15,6 +15,7 @@ import type {SideEffectTag} from 'shared/ReactSideEffectTags';
15 import type {ExpirationTime} from './ReactFiberExpirationTime';
16 import type {UpdateQueue} from './ReactUpdateQueue';
17 import type {ContextDependencyList} from './ReactFiberNewContext';
18 +import type {HookType} from './ReactFiberHooks';
19
20 import invariant from 'shared/invariant';
21 import warningWithoutStack from 'shared/warningWithoutStack';
@@ -204,6 +205,9 @@ export type Fiber = {|
205 _debugSource?: Source | null,
206 _debugOwner?: Fiber | null,
207 _debugIsCurrentlyTiming?: boolean,
208 +
209 + // Used to verify that the order of hooks does not change between renders.
210 + _debugHookTypes?: Array<HookType> | null,
211 |};
212
213 let debugCounter;
@@ -285,6 +289,7 @@ function FiberNode(
289 this._debugSource = null;
290 this._debugOwner = null;
291 this._debugIsCurrentlyTiming = false;
292 + this._debugHookTypes = null;
293 if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
294 Object.preventExtensions(this);
295 }
@@ -370,6 +375,7 @@ export function createWorkInProgress(
375 workInProgress._debugID = current._debugID;
376 workInProgress._debugSource = current._debugSource;
377 workInProgress._debugOwner = current._debugOwner;
378 + workInProgress._debugHookTypes = current._debugHookTypes;
379 }
380
381 workInProgress.alternate = current;
@@ -723,5 +729,6 @@ export function assignFiberPropertiesInDEV(
729 target._debugSource = source._debugSource;
730 target._debugOwner = source._debugOwner;
731 target._debugIsCurrentlyTiming = source._debugIsCurrentlyTiming;
732 + target._debugHookTypes = source._debugHookTypes;
733 return target;
734 }
packages/react-reconciler/src/ReactFiberHooks.js
+260 -91
@@ -93,7 +93,7 @@ type UpdateQueue<S, A> = {
93 eagerState: S | null,
94 };
95
96 -type HookType =
96 +export type HookType =
97 | 'useState'
98 | 'useReducer'
99 | 'useContext'
@@ -120,10 +120,6 @@ export type Hook = {
120 next: Hook | null,
121 };
122
123 -type HookDev = Hook & {
124 - _debugType: HookType,
125 -};
126 -
123 type Effect = {
124 tag: HookEffectTag,
125 create: () => (() => void) | void,
@@ -150,7 +146,6 @@ let currentlyRenderingFiber: Fiber | null = null;
146 // current hook list is the list that belongs to the current fiber. The
147 // work-in-progress hook list is a new list that will be added to the
148 // work-in-progress fiber.
153 -let firstCurrentHook: Hook | null = null;
149 let currentHook: Hook | null = null;
150 let nextCurrentHook: Hook | null = null;
151 let firstWorkInProgressHook: Hook | null = null;
@@ -183,7 +178,38 @@ const RE_RENDER_LIMIT = 25;
178 // In DEV, this is the name of the currently executing primitive hook
179 let currentHookNameInDev: ?HookType = null;
180
186 -function warnOnHookMismatchInDev() {
181 +// In DEV, this list ensures that hooks are called in the same order between renders.
182 +// The list stores the order of hooks used during the initial render (mount).
183 +// Subsequent renders (updates) reference this list.
184 +let hookTypesDev: Array<HookType> | null = null;
185 +let hookTypesUpdateIndexDev: number = -1;
186 +
187 +function mountHookTypesDev() {
188 + if (__DEV__) {
189 + const hookName = ((currentHookNameInDev: any): HookType);
190 +
191 + if (hookTypesDev === null) {
192 + hookTypesDev = [hookName];
193 + } else {
194 + hookTypesDev.push(hookName);
195 + }
196 + }
197 +}
198 +
199 +function updateHookTypesDev() {
200 + if (__DEV__) {
201 + const hookName = ((currentHookNameInDev: any): HookType);
202 +
203 + if (hookTypesDev !== null) {
204 + hookTypesUpdateIndexDev++;
205 + if (hookTypesDev[hookTypesUpdateIndexDev] !== hookName) {
206 + warnOnHookMismatchInDev(hookName);
207 + }
208 + }
209 + }
210 +}
211 +
212 +function warnOnHookMismatchInDev(currentHookName: HookType) {
213 if (__DEV__) {
214 const componentName = getComponentName(
215 ((currentlyRenderingFiber: any): Fiber).type,
@@ -191,44 +217,44 @@ function warnOnHookMismatchInDev() {
217 if (!didWarnAboutMismatchedHooksForComponent.has(componentName)) {
218 didWarnAboutMismatchedHooksForComponent.add(componentName);
219
194 - const secondColumnStart = 22;
220 + if (hookTypesDev !== null) {
221 + let table = '';
222
196 - let table = '';
197 - let prevHook: HookDev | null = (firstCurrentHook: any);
198 - let nextHook: HookDev | null = (firstWorkInProgressHook: any);
199 - let n = 1;
200 - while (prevHook !== null && nextHook !== null) {
201 - const oldHookName = prevHook._debugType;
202 - const newHookName = nextHook._debugType;
223 + const secondColumnStart = 30;
224
204 - let row = `${n}. ${oldHookName}`;
225 + for (let i = 0; i <= ((hookTypesUpdateIndexDev: any): number); i++) {
226 + const oldHookName = hookTypesDev[i];
227 + const newHookName =
228 + i === ((hookTypesUpdateIndexDev: any): number)
229 + ? currentHookName
230 + : oldHookName;
231
206 - // Extra space so second column lines up
207 - // lol @ IE not supporting String#repeat
208 - while (row.length < secondColumnStart) {
209 - row += ' ';
210 - }
232 + let row = `${i + 1}. ${oldHookName}`;
233
212 - row += newHookName + '\n';
234 + // Extra space so second column lines up
235 + // lol @ IE not supporting String#repeat
236 + while (row.length < secondColumnStart) {
237 + row += ' ';
238 + }
239
214 - table += row;
215 - prevHook = (prevHook.next: any);
216 - nextHook = (nextHook.next: any);
217 - n++;
218 - }
240 + row += newHookName + '\n';
241
220 - warning(
221 - false,
222 - 'React has detected a change in the order of Hooks called by %s. ' +
223 - 'This will lead to bugs and errors if not fixed. ' +
224 - 'For more information, read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
225 - ' Previous render Next render\n' +
226 - ' -------------------------------\n' +
227 - '%s' +
228 - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n',
229 - componentName,
230 - table,
231 - );
242 + table += row;
243 + }
244 +
245 + warning(
246 + false,
247 + 'React has detected a change in the order of Hooks called by %s. ' +
248 + 'This will lead to bugs and errors if not fixed. ' +
249 + 'For more information, read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
250 + ' Previous render Next render\n' +
251 + ' ------------------------------------------------------\n' +
252 + '%s' +
253 + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n',
254 + componentName,
255 + table,
256 + );
257 + }
258 }
259 }
260 }
@@ -293,8 +319,15 @@ export function renderWithHooks(
319 ): any {
320 renderExpirationTime = nextRenderExpirationTime;
321 currentlyRenderingFiber = workInProgress;
296 - firstCurrentHook = nextCurrentHook =
297 - current !== null ? current.memoizedState : null;
322 + nextCurrentHook = current !== null ? current.memoizedState : null;
323 +
324 + if (__DEV__) {
325 + hookTypesDev =
326 + current !== null
327 + ? ((current._debugHookTypes: any): Array<HookType>)
328 + : null;
329 + hookTypesUpdateIndexDev = -1;
330 + }
331
332 // The following should have already been reset
333 // currentHook = null;
@@ -308,11 +341,26 @@ export function renderWithHooks(
341 // numberOfReRenders = 0;
342 // sideEffectTag = 0;
343
344 + // TODO Warn if no hooks are used at all during mount, then some are used during update.
345 + // Currently we will identify the update render as a mount because nextCurrentHook === null.
346 + // This is tricky because it's valid for certain types of components (e.g. React.lazy)
347 +
348 + // Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used.
349 + // Non-stateful hooks (e.g. context) don't get added to memoizedState,
350 + // so nextCurrentHook would be null during updates and mounts.
351 if (__DEV__) {
312 - ReactCurrentDispatcher.current =
313 - nextCurrentHook === null
314 - ? HooksDispatcherOnMountInDEV
315 - : HooksDispatcherOnUpdateInDEV;
352 + if (nextCurrentHook !== null) {
353 + ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
354 + } else if (hookTypesDev !== null) {
355 + // This dispatcher handles an edge case where a component is updating,
356 + // but no stateful hooks have been used.
357 + // We want to match the production code behavior (which will use HooksDispatcherOnMount),
358 + // but with the extra DEV validation to ensure hooks ordering hasn't changed.
359 + // This dispatcher does that.
360 + ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
361 + } else {
362 + ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
363 + }
364 } else {
365 ReactCurrentDispatcher.current =
366 nextCurrentHook === null
@@ -328,14 +376,18 @@ export function renderWithHooks(
376 numberOfReRenders += 1;
377
378 // Start over from the beginning of the list
331 - firstCurrentHook = nextCurrentHook =
332 - current !== null ? current.memoizedState : null;
379 + nextCurrentHook = current !== null ? current.memoizedState : null;
380 nextWorkInProgressHook = firstWorkInProgressHook;
381
382 currentHook = null;
383 workInProgressHook = null;
384 componentUpdateQueue = null;
385
386 + if (__DEV__) {
387 + // Also validate hook order for cascading updates.
388 + hookTypesUpdateIndexDev = -1;
389 + }
390 +
391 ReactCurrentDispatcher.current = __DEV__
392 ? HooksDispatcherOnUpdateInDEV
393 : HooksDispatcherOnUpdate;
@@ -347,10 +399,6 @@ export function renderWithHooks(
399 numberOfReRenders = 0;
400 }
401
350 - if (__DEV__) {
351 - currentHookNameInDev = null;
352 - }
353 -
402 // We can assume the previous dispatcher is always this one, since we set it
403 // at the beginning of the render phase and there's no re-entrancy.
404 ReactCurrentDispatcher.current = ContextOnlyDispatcher;
@@ -362,19 +410,30 @@ export function renderWithHooks(
410 renderedWork.updateQueue = (componentUpdateQueue: any);
411 renderedWork.effectTag |= sideEffectTag;
412
413 + if (__DEV__) {
414 + renderedWork._debugHookTypes = hookTypesDev;
415 + }
416 +
417 + // This check uses currentHook so that it works the same in DEV and prod bundles.
418 + // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
419 const didRenderTooFewHooks =
420 currentHook !== null && currentHook.next !== null;
421
422 renderExpirationTime = NoWork;
423 currentlyRenderingFiber = null;
424
371 - firstCurrentHook = null;
425 currentHook = null;
426 nextCurrentHook = null;
427 firstWorkInProgressHook = null;
428 workInProgressHook = null;
429 nextWorkInProgressHook = null;
430
431 + if (__DEV__) {
432 + currentHookNameInDev = null;
433 + hookTypesDev = null;
434 + hookTypesUpdateIndexDev = -1;
435 + }
436 +
437 remainingExpirationTime = NoWork;
438 componentUpdateQueue = null;
439 sideEffectTag = 0;
@@ -416,21 +475,23 @@ export function resetHooks(): void {
475 renderExpirationTime = NoWork;
476 currentlyRenderingFiber = null;
477
419 - firstCurrentHook = null;
478 currentHook = null;
479 nextCurrentHook = null;
480 firstWorkInProgressHook = null;
481 workInProgressHook = null;
482 nextWorkInProgressHook = null;
483
426 - remainingExpirationTime = NoWork;
427 - componentUpdateQueue = null;
428 - sideEffectTag = 0;
429 -
484 if (__DEV__) {
485 + hookTypesDev = null;
486 + hookTypesUpdateIndexDev = -1;
487 +
488 currentHookNameInDev = null;
489 }
490
491 + remainingExpirationTime = NoWork;
492 + componentUpdateQueue = null;
493 + sideEffectTag = 0;
494 +
495 didScheduleRenderPhaseUpdate = false;
496 renderPhaseUpdates = null;
497 numberOfReRenders = 0;
@@ -447,9 +508,6 @@ function mountWorkInProgressHook(): Hook {
508 next: null,
509 };
510
450 - if (__DEV__) {
451 - (hook: any)._debugType = (currentHookNameInDev: any);
452 - }
511 if (workInProgressHook === null) {
512 // This is the first hook in the list
513 firstWorkInProgressHook = workInProgressHook = hook;
@@ -499,13 +557,6 @@ function updateWorkInProgressHook(): Hook {
557 workInProgressHook = workInProgressHook.next = newHook;
558 }
559 nextCurrentHook = currentHook.next;
502 -
503 - if (__DEV__) {
504 - (newHook: any)._debugType = (currentHookNameInDev: any);
505 - if (currentHookNameInDev !== ((currentHook: any): HookDev)._debugType) {
506 - warnOnHookMismatchInDev();
507 - }
508 - }
560 }
561 return workInProgressHook;
562 }
@@ -520,26 +571,6 @@ function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
571 return typeof action === 'function' ? action(state) : action;
572 }
573
523 -function mountContext<T>(
524 - context: ReactContext<T>,
525 - observedBits: void | number | boolean,
526 -): T {
527 - if (__DEV__) {
528 - mountWorkInProgressHook();
529 - }
530 - return readContext(context, observedBits);
531 -}
532 -
533 -function updateContext<T>(
534 - context: ReactContext<T>,
535 - observedBits: void | number | boolean,
536 -): T {
537 - if (__DEV__) {
538 - updateWorkInProgressHook();
539 - }
540 - return readContext(context, observedBits);
541 -}
542 -
574 function mountReducer<S, I, A>(
575 reducer: (S, A) => S,
576 initialArg: I,
@@ -1181,6 +1212,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
1212 };
1213
1214 let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
1215 +let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
1216 let HooksDispatcherOnUpdateInDEV: Dispatcher | null = null;
1217 let InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher | null = null;
1218 let InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher | null = null;
@@ -1216,6 +1248,104 @@ if (__DEV__) {
1248
1249 useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
1250 currentHookNameInDev = 'useCallback';
1251 + mountHookTypesDev();
1252 + return mountCallback(callback, deps);
1253 + },
1254 + useContext<T>(
1255 + context: ReactContext<T>,
1256 + observedBits: void | number | boolean,
1257 + ): T {
1258 + currentHookNameInDev = 'useContext';
1259 + mountHookTypesDev();
1260 + return readContext(context, observedBits);
1261 + },
1262 + useEffect(
1263 + create: () => (() => void) | void,
1264 + deps: Array<mixed> | void | null,
1265 + ): void {
1266 + currentHookNameInDev = 'useEffect';
1267 + mountHookTypesDev();
1268 + return mountEffect(create, deps);
1269 + },
1270 + useImperativeHandle<T>(
1271 + ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
1272 + create: () => T,
1273 + deps: Array<mixed> | void | null,
1274 + ): void {
1275 + currentHookNameInDev = 'useImperativeHandle';
1276 + mountHookTypesDev();
1277 + return mountImperativeHandle(ref, create, deps);
1278 + },
1279 + useLayoutEffect(
1280 + create: () => (() => void) | void,
1281 + deps: Array<mixed> | void | null,
1282 + ): void {
1283 + currentHookNameInDev = 'useLayoutEffect';
1284 + mountHookTypesDev();
1285 + return mountLayoutEffect(create, deps);
1286 + },
1287 + useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
1288 + currentHookNameInDev = 'useMemo';
1289 + mountHookTypesDev();
1290 + const prevDispatcher = ReactCurrentDispatcher.current;
1291 + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1292 + try {
1293 + return mountMemo(create, deps);
1294 + } finally {
1295 + ReactCurrentDispatcher.current = prevDispatcher;
1296 + }
1297 + },
1298 + useReducer<S, I, A>(
1299 + reducer: (S, A) => S,
1300 + initialArg: I,
1301 + init?: I => S,
1302 + ): [S, Dispatch<A>] {
1303 + currentHookNameInDev = 'useReducer';
1304 + mountHookTypesDev();
1305 + const prevDispatcher = ReactCurrentDispatcher.current;
1306 + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1307 + try {
1308 + return mountReducer(reducer, initialArg, init);
1309 + } finally {
1310 + ReactCurrentDispatcher.current = prevDispatcher;
1311 + }
1312 + },
1313 + useRef<T>(initialValue: T): {current: T} {
1314 + currentHookNameInDev = 'useRef';
1315 + mountHookTypesDev();
1316 + return mountRef(initialValue);
1317 + },
1318 + useState<S>(
1319 + initialState: (() => S) | S,
1320 + ): [S, Dispatch<BasicStateAction<S>>] {
1321 + currentHookNameInDev = 'useState';
1322 + mountHookTypesDev();
1323 + const prevDispatcher = ReactCurrentDispatcher.current;
1324 + ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1325 + try {
1326 + return mountState(initialState);
1327 + } finally {
1328 + ReactCurrentDispatcher.current = prevDispatcher;
1329 + }
1330 + },
1331 + useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
1332 + currentHookNameInDev = 'useDebugValue';
1333 + mountHookTypesDev();
1334 + return mountDebugValue(value, formatterFn);
1335 + },
1336 + };
1337 +
1338 + HooksDispatcherOnMountWithHookTypesInDEV = {
1339 + readContext<T>(
1340 + context: ReactContext<T>,
1341 + observedBits: void | number | boolean,
1342 + ): T {
1343 + return readContext(context, observedBits);
1344 + },
1345 +
1346 + useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
1347 + currentHookNameInDev = 'useCallback';
1348 + updateHookTypesDev();
1349 return mountCallback(callback, deps);
1350 },
1351 useContext<T>(
@@ -1223,13 +1353,15 @@ if (__DEV__) {
1353 observedBits: void | number | boolean,
1354 ): T {
1355 currentHookNameInDev = 'useContext';
1226 - return mountContext(context, observedBits);
1356 + updateHookTypesDev();
1357 + return readContext(context, observedBits);
1358 },
1359 useEffect(
1360 create: () => (() => void) | void,
1361 deps: Array<mixed> | void | null,
1362 ): void {
1363 currentHookNameInDev = 'useEffect';
1364 + updateHookTypesDev();
1365 return mountEffect(create, deps);
1366 },
1367 useImperativeHandle<T>(
@@ -1238,6 +1370,7 @@ if (__DEV__) {
1370 deps: Array<mixed> | void | null,
1371 ): void {
1372 currentHookNameInDev = 'useImperativeHandle';
1373 + updateHookTypesDev();
1374 return mountImperativeHandle(ref, create, deps);
1375 },
1376 useLayoutEffect(
@@ -1245,10 +1378,12 @@ if (__DEV__) {
1378 deps: Array<mixed> | void | null,
1379 ): void {
1380 currentHookNameInDev = 'useLayoutEffect';
1381 + updateHookTypesDev();
1382 return mountLayoutEffect(create, deps);
1383 },
1384 useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
1385 currentHookNameInDev = 'useMemo';
1386 + updateHookTypesDev();
1387 const prevDispatcher = ReactCurrentDispatcher.current;
1388 ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1389 try {
@@ -1263,6 +1398,7 @@ if (__DEV__) {
1398 init?: I => S,
1399 ): [S, Dispatch<A>] {
1400 currentHookNameInDev = 'useReducer';
1401 + updateHookTypesDev();
1402 const prevDispatcher = ReactCurrentDispatcher.current;
1403 ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1404 try {
@@ -1273,12 +1409,14 @@ if (__DEV__) {
1409 },
1410 useRef<T>(initialValue: T): {current: T} {
1411 currentHookNameInDev = 'useRef';
1412 + updateHookTypesDev();
1413 return mountRef(initialValue);
1414 },
1415 useState<S>(
1416 initialState: (() => S) | S,
1417 ): [S, Dispatch<BasicStateAction<S>>] {
1418 currentHookNameInDev = 'useState';
1419 + updateHookTypesDev();
1420 const prevDispatcher = ReactCurrentDispatcher.current;
1421 ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1422 try {
@@ -1289,6 +1427,7 @@ if (__DEV__) {
1427 },
1428 useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
1429 currentHookNameInDev = 'useDebugValue';
1430 + updateHookTypesDev();
1431 return mountDebugValue(value, formatterFn);
1432 },
1433 };
@@ -1303,6 +1442,7 @@ if (__DEV__) {
1442
1443 useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
1444 currentHookNameInDev = 'useCallback';
1445 + updateHookTypesDev();
1446 return updateCallback(callback, deps);
1447 },
1448 useContext<T>(
@@ -1310,13 +1450,15 @@ if (__DEV__) {
1450 observedBits: void | number | boolean,
1451 ): T {
1452 currentHookNameInDev = 'useContext';
1313 - return updateContext(context, observedBits);
1453 + updateHookTypesDev();
1454 + return readContext(context, observedBits);
1455 },
1456 useEffect(
1457 create: () => (() => void) | void,
1458 deps: Array<mixed> | void | null,
1459 ): void {
1460 currentHookNameInDev = 'useEffect';
1461 + updateHookTypesDev();
1462 return updateEffect(create, deps);
1463 },
1464 useImperativeHandle<T>(
@@ -1325,6 +1467,7 @@ if (__DEV__) {
1467 deps: Array<mixed> | void | null,
1468 ): void {
1469 currentHookNameInDev = 'useImperativeHandle';
1470 + updateHookTypesDev();
1471 return updateImperativeHandle(ref, create, deps);
1472 },
1473 useLayoutEffect(
@@ -1332,10 +1475,12 @@ if (__DEV__) {
1475 deps: Array<mixed> | void | null,
1476 ): void {
1477 currentHookNameInDev = 'useLayoutEffect';
1478 + updateHookTypesDev();
1479 return updateLayoutEffect(create, deps);
1480 },
1481 useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
1482 currentHookNameInDev = 'useMemo';
1483 + updateHookTypesDev();
1484 const prevDispatcher = ReactCurrentDispatcher.current;
1485 ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
1486 try {
@@ -1350,6 +1495,7 @@ if (__DEV__) {
1495 init?: I => S,
1496 ): [S, Dispatch<A>] {
1497 currentHookNameInDev = 'useReducer';
1498 + updateHookTypesDev();
1499 const prevDispatcher = ReactCurrentDispatcher.current;
1500 ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
1501 try {
@@ -1360,12 +1506,14 @@ if (__DEV__) {
1506 },
1507 useRef<T>(initialValue: T): {current: T} {
1508 currentHookNameInDev = 'useRef';
1509 + updateHookTypesDev();
1510 return updateRef(initialValue);
1511 },
1512 useState<S>(
1513 initialState: (() => S) | S,
1514 ): [S, Dispatch<BasicStateAction<S>>] {
1515 currentHookNameInDev = 'useState';
1516 + updateHookTypesDev();
1517 const prevDispatcher = ReactCurrentDispatcher.current;
1518 ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
1519 try {
@@ -1376,6 +1524,7 @@ if (__DEV__) {
1524 },
1525 useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
1526 currentHookNameInDev = 'useDebugValue';
1527 + updateHookTypesDev();
1528 return updateDebugValue(value, formatterFn);
1529 },
1530 };
@@ -1392,6 +1541,7 @@ if (__DEV__) {
1541 useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
1542 currentHookNameInDev = 'useCallback';
1543 warnInvalidHookAccess();
1544 + mountHookTypesDev();
1545 return mountCallback(callback, deps);
1546 },
1547 useContext<T>(
@@ -1400,7 +1550,8 @@ if (__DEV__) {
1550 ): T {
1551 currentHookNameInDev = 'useContext';
1552 warnInvalidHookAccess();
1403 - return mountContext(context, observedBits);
1553 + mountHookTypesDev();
1554 + return readContext(context, observedBits);
1555 },
1556 useEffect(
1557 create: () => (() => void) | void,
@@ -1408,6 +1559,7 @@ if (__DEV__) {
1559 ): void {
1560 currentHookNameInDev = 'useEffect';
1561 warnInvalidHookAccess();
1562 + mountHookTypesDev();
1563 return mountEffect(create, deps);
1564 },
1565 useImperativeHandle<T>(
@@ -1417,6 +1569,7 @@ if (__DEV__) {
1569 ): void {
1570 currentHookNameInDev = 'useImperativeHandle';
1571 warnInvalidHookAccess();
1572 + mountHookTypesDev();
1573 return mountImperativeHandle(ref, create, deps);
1574 },
1575 useLayoutEffect(
@@ -1425,11 +1578,13 @@ if (__DEV__) {
1578 ): void {
1579 currentHookNameInDev = 'useLayoutEffect';
1580 warnInvalidHookAccess();
1581 + mountHookTypesDev();
1582 return mountLayoutEffect(create, deps);
1583 },
1584 useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
1585 currentHookNameInDev = 'useMemo';
1586 warnInvalidHookAccess();
1587 + mountHookTypesDev();
1588 const prevDispatcher = ReactCurrentDispatcher.current;
1589 ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1590 try {
@@ -1445,6 +1600,7 @@ if (__DEV__) {
1600 ): [S, Dispatch<A>] {
1601 currentHookNameInDev = 'useReducer';
1602 warnInvalidHookAccess();
1603 + mountHookTypesDev();
1604 const prevDispatcher = ReactCurrentDispatcher.current;
1605 ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1606 try {
@@ -1456,6 +1612,7 @@ if (__DEV__) {
1612 useRef<T>(initialValue: T): {current: T} {
1613 currentHookNameInDev = 'useRef';
1614 warnInvalidHookAccess();
1615 + mountHookTypesDev();
1616 return mountRef(initialValue);
1617 },
1618 useState<S>(
@@ -1463,6 +1620,7 @@ if (__DEV__) {
1620 ): [S, Dispatch<BasicStateAction<S>>] {
1621 currentHookNameInDev = 'useState';
1622 warnInvalidHookAccess();
1623 + mountHookTypesDev();
1624 const prevDispatcher = ReactCurrentDispatcher.current;
1625 ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
1626 try {
@@ -1474,6 +1632,7 @@ if (__DEV__) {
1632 useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
1633 currentHookNameInDev = 'useDebugValue';
1634 warnInvalidHookAccess();
1635 + mountHookTypesDev();
1636 return mountDebugValue(value, formatterFn);
1637 },
1638 };
@@ -1490,6 +1649,7 @@ if (__DEV__) {
1649 useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
1650 currentHookNameInDev = 'useCallback';
1651 warnInvalidHookAccess();
1652 + updateHookTypesDev();
1653 return updateCallback(callback, deps);
1654 },
1655 useContext<T>(
@@ -1498,7 +1658,8 @@ if (__DEV__) {
1658 ): T {
1659 currentHookNameInDev = 'useContext';
1660 warnInvalidHookAccess();
1501 - return updateContext(context, observedBits);
1661 + updateHookTypesDev();
1662 + return readContext(context, observedBits);
1663 },
1664 useEffect(
1665 create: () => (() => void) | void,
@@ -1506,6 +1667,7 @@ if (__DEV__) {
1667 ): void {
1668 currentHookNameInDev = 'useEffect';
1669 warnInvalidHookAccess();
1670 + updateHookTypesDev();
1671 return updateEffect(create, deps);
1672 },
1673 useImperativeHandle<T>(
@@ -1515,6 +1677,7 @@ if (__DEV__) {
1677 ): void {
1678 currentHookNameInDev = 'useImperativeHandle';
1679 warnInvalidHookAccess();
1680 + updateHookTypesDev();
1681 return updateImperativeHandle(ref, create, deps);
1682 },
1683 useLayoutEffect(
@@ -1523,11 +1686,13 @@ if (__DEV__) {
1686 ): void {
1687 currentHookNameInDev = 'useLayoutEffect';
1688 warnInvalidHookAccess();
1689 + updateHookTypesDev();
1690 return updateLayoutEffect(create, deps);
1691 },
1692 useMemo<T>(create: () => T, deps: Array<mixed> | void | null): T {
1693 currentHookNameInDev = 'useMemo';
1694 warnInvalidHookAccess();
1695 + updateHookTypesDev();
1696 const prevDispatcher = ReactCurrentDispatcher.current;
1697 ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
1698 try {
@@ -1543,6 +1708,7 @@ if (__DEV__) {
1708 ): [S, Dispatch<A>] {
1709 currentHookNameInDev = 'useReducer';
1710 warnInvalidHookAccess();
1711 + updateHookTypesDev();
1712 const prevDispatcher = ReactCurrentDispatcher.current;
1713 ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
1714 try {
@@ -1554,6 +1720,7 @@ if (__DEV__) {
1720 useRef<T>(initialValue: T): {current: T} {
1721 currentHookNameInDev = 'useRef';
1722 warnInvalidHookAccess();
1723 + updateHookTypesDev();
1724 return updateRef(initialValue);
1725 },
1726 useState<S>(
@@ -1561,6 +1728,7 @@ if (__DEV__) {
1728 ): [S, Dispatch<BasicStateAction<S>>] {
1729 currentHookNameInDev = 'useState';
1730 warnInvalidHookAccess();
1731 + updateHookTypesDev();
1732 const prevDispatcher = ReactCurrentDispatcher.current;
1733 ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
1734 try {
@@ -1572,6 +1740,7 @@ if (__DEV__) {
1740 useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
1741 currentHookNameInDev = 'useDebugValue';
1742 warnInvalidHookAccess();
1743 + updateHookTypesDev();
1744 return updateDebugValue(value, formatterFn);
1745 },
1746 };
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+268 -76
@@ -984,8 +984,6 @@ describe('ReactHooks', () => {
984 it('warns when calling hooks inside useReducer', () => {
985 const {useReducer, useState, useRef} = React;
986
987 - spyOnDev(console, 'error');
988 -
987 function App() {
988 const [value, dispatch] = useReducer((state, action) => {
989 useRef(0);
@@ -997,16 +995,23 @@ describe('ReactHooks', () => {
995 useState();
996 return value;
997 }
1000 - expect(() => {
1001 - ReactTestRenderer.create(<App />);
1002 - }).toThrow('Rendered more hooks than during the previous render.');
998
1004 - if (__DEV__) {
1005 - expect(console.error).toHaveBeenCalledTimes(3);
1006 - expect(console.error.calls.argsFor(0)[0]).toContain(
1007 - 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
1008 - );
1009 - }
999 + expect(() => {
1000 + expect(() => {
1001 + ReactTestRenderer.create(<App />);
1002 + }).toThrow('Rendered more hooks than during the previous render.');
1003 + }).toWarnDev([
1004 + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
1005 + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
1006 + 'Warning: React has detected a change in the order of Hooks called by App. ' +
1007 + 'This will lead to bugs and errors if not fixed. For more information, ' +
1008 + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
1009 + ' Previous render Next render\n' +
1010 + ' ------------------------------------------------------\n' +
1011 + '1. useReducer useReducer\n' +
1012 + '2. useState useRef\n' +
1013 + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
1014 + ]);
1015 });
1016
1017 it("warns when calling hooks inside useState's initialize function", () => {
@@ -1337,74 +1342,261 @@ describe('ReactHooks', () => {
1342 expect(useMemoCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
1343 });
1344
1340 - it('warns on using differently ordered hooks on subsequent renders', () => {
1341 - const {useState, useReducer, useRef} = React;
1342 - function useCustomHook() {
1343 - return useState(0);
1344 - }
1345 - function App(props) {
1346 - /* eslint-disable no-unused-vars */
1347 - if (props.flip) {
1348 - useCustomHook(0);
1349 - useReducer((s, a) => a, 0);
1350 - } else {
1351 - useReducer((s, a) => a, 0);
1352 - useCustomHook(0);
1353 - }
1354 - // This should not appear in the warning message because it occurs after
1355 - // the first mismatch
1356 - const ref = useRef(null);
1357 - return null;
1358 - /* eslint-enable no-unused-vars */
1359 - }
1360 - let root = ReactTestRenderer.create(<App flip={false} />);
1361 - expect(() => {
1362 - root.update(<App flip={true} />);
1363 - }).toWarnDev([
1364 - 'Warning: React has detected a change in the order of Hooks called by App. ' +
1365 - 'This will lead to bugs and errors if not fixed. For more information, ' +
1366 - 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
1367 - ' Previous render Next render\n' +
1368 - ' -------------------------------\n' +
1369 - '1. useReducer useState\n' +
1370 - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' +
1371 - ' in App (at **)',
1372 - ]);
1345 + describe('hook ordering', () => {
1346 + const useCallbackHelper = () => React.useCallback(() => {}, []);
1347 + const useContextHelper = () => React.useContext(React.createContext());
1348 + const useDebugValueHelper = () => React.useDebugValue('abc');
1349 + const useEffectHelper = () => React.useEffect(() => () => {}, []);
1350 + const useImperativeHandleHelper = () => {
1351 + React.useImperativeHandle({current: null}, () => ({}), []);
1352 + };
1353 + const useLayoutEffectHelper = () =>
1354 + React.useLayoutEffect(() => () => {}, []);
1355 + const useMemoHelper = () => React.useMemo(() => 123, []);
1356 + const useReducerHelper = () => React.useReducer((s, a) => a, 0);
1357 + const useRefHelper = () => React.useRef(null);
1358 + const useStateHelper = () => React.useState(0);
1359 +
1360 + // We don't include useImperativeHandleHelper in this set,
1361 + // because it generates an additional warning about the inputs length changing.
1362 + // We test it below with its own test.
1363 + let orderedHooks = [
1364 + useCallbackHelper,
1365 + useContextHelper,
1366 + useDebugValueHelper,
1367 + useEffectHelper,
1368 + useLayoutEffectHelper,
1369 + useMemoHelper,
1370 + useReducerHelper,
1371 + useRefHelper,
1372 + useStateHelper,
1373 + ];
1374 +
1375 + const formatHookNamesToMatchErrorMessage = (hookNameA, hookNameB) => {
1376 + return `use${hookNameA}${' '.repeat(24 - hookNameA.length)}${
1377 + hookNameB ? `use${hookNameB}` : undefined
1378 + }`;
1379 + };
1380
1374 - // further warnings for this component are silenced
1375 - root.update(<App flip={false} />);
1376 - });
1381 + orderedHooks.forEach((firstHelper, index) => {
1382 + const secondHelper =
1383 + index > 0
1384 + ? orderedHooks[index - 1]
1385 + : orderedHooks[orderedHooks.length - 1];
1386 +
1387 + const hookNameA = firstHelper.name
1388 + .replace('use', '')
1389 + .replace('Helper', '');
1390 + const hookNameB = secondHelper.name
1391 + .replace('use', '')
1392 + .replace('Helper', '');
1393 +
1394 + it(`warns on using differently ordered hooks (${hookNameA}, ${hookNameB}) on subsequent renders`, () => {
1395 + function App(props) {
1396 + /* eslint-disable no-unused-vars */
1397 + if (props.update) {
1398 + secondHelper();
1399 + firstHelper();
1400 + } else {
1401 + firstHelper();
1402 + secondHelper();
1403 + }
1404 + // This should not appear in the warning message because it occurs after the first mismatch
1405 + useRefHelper();
1406 + return null;
1407 + /* eslint-enable no-unused-vars */
1408 + }
1409 + let root = ReactTestRenderer.create(<App update={false} />);
1410 + expect(() => {
1411 + try {
1412 + root.update(<App update={true} />);
1413 + } catch (error) {
1414 + // Swapping certain types of hooks will cause runtime errors.
1415 + // This is okay as far as this test is concerned.
1416 + // We just want to verify that warnings are always logged.
1417 + }
1418 + }).toWarnDev([
1419 + 'Warning: React has detected a change in the order of Hooks called by App. ' +
1420 + 'This will lead to bugs and errors if not fixed. For more information, ' +
1421 + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
1422 + ' Previous render Next render\n' +
1423 + ' ------------------------------------------------------\n' +
1424 + `1. ${formatHookNamesToMatchErrorMessage(hookNameA, hookNameB)}\n` +
1425 + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' +
1426 + ' in App (at **)',
1427 + ]);
1428 +
1429 + // further warnings for this component are silenced
1430 + try {
1431 + root.update(<App update={false} />);
1432 + } catch (error) {
1433 + // Swapping certain types of hooks will cause runtime errors.
1434 + // This is okay as far as this test is concerned.
1435 + // We just want to verify that warnings are always logged.
1436 + }
1437 + });
1438
1378 - it('detects a bad hook order even if the component throws', () => {
1379 - const {useState, useReducer} = React;
1380 - function useCustomHook() {
1381 - useState(0);
1382 - }
1383 - function App(props) {
1384 - /* eslint-disable no-unused-vars */
1385 - if (props.flip) {
1386 - useCustomHook();
1387 - useReducer((s, a) => a, 0);
1388 - throw new Error('custom error');
1389 - } else {
1390 - useReducer((s, a) => a, 0);
1391 - useCustomHook();
1439 + it(`warns when more hooks (${(hookNameA,
1440 + hookNameB)}) are used during update than mount`, () => {
1441 + function App(props) {
1442 + /* eslint-disable no-unused-vars */
1443 + if (props.update) {
1444 + firstHelper();
1445 + secondHelper();
1446 + } else {
1447 + firstHelper();
1448 + }
1449 + return null;
1450 + /* eslint-enable no-unused-vars */
1451 + }
1452 + let root = ReactTestRenderer.create(<App update={false} />);
1453 + expect(() => {
1454 + try {
1455 + root.update(<App update={true} />);
1456 + } catch (error) {
1457 + // Swapping certain types of hooks will cause runtime errors.
1458 + // This is okay as far as this test is concerned.
1459 + // We just want to verify that warnings are always logged.
1460 + }
1461 + }).toWarnDev([
1462 + 'Warning: React has detected a change in the order of Hooks called by App. ' +
1463 + 'This will lead to bugs and errors if not fixed. For more information, ' +
1464 + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
1465 + ' Previous render Next render\n' +
1466 + ' ------------------------------------------------------\n' +
1467 + `1. ${formatHookNamesToMatchErrorMessage(hookNameA, hookNameA)}\n` +
1468 + `2. undefined use${hookNameB}\n` +
1469 + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' +
1470 + ' in App (at **)',
1471 + ]);
1472 + });
1473 + });
1474 +
1475 + // We don't include useContext or useDebugValue in this set,
1476 + // because they aren't added to the hooks list and so won't throw.
1477 + let hooksInList = [
1478 + useCallbackHelper,
1479 + useEffectHelper,
1480 + useImperativeHandleHelper,
1481 + useLayoutEffectHelper,
1482 + useMemoHelper,
1483 + useReducerHelper,
1484 + useRefHelper,
1485 + useStateHelper,
1486 + ];
1487 +
1488 + hooksInList.forEach((firstHelper, index) => {
1489 + const secondHelper =
1490 + index > 0
1491 + ? hooksInList[index - 1]
1492 + : hooksInList[hooksInList.length - 1];
1493 +
1494 + const hookNameA = firstHelper.name
1495 + .replace('use', '')
1496 + .replace('Helper', '');
1497 + const hookNameB = secondHelper.name
1498 + .replace('use', '')
1499 + .replace('Helper', '');
1500 +
1501 + it(`warns when fewer hooks (${(hookNameA,
1502 + hookNameB)}) are used during update than mount`, () => {
1503 + function App(props) {
1504 + /* eslint-disable no-unused-vars */
1505 + if (props.update) {
1506 + firstHelper();
1507 + } else {
1508 + firstHelper();
1509 + secondHelper();
1510 + }
1511 + return null;
1512 + /* eslint-enable no-unused-vars */
1513 + }
1514 + let root = ReactTestRenderer.create(<App update={false} />);
1515 + expect(() => {
1516 + root.update(<App update={true} />);
1517 + }).toThrow('Rendered fewer hooks than expected.');
1518 + });
1519 + });
1520 +
1521 + it(
1522 + 'warns on using differently ordered hooks ' +
1523 + '(useImperativeHandleHelper, useMemoHelper) on subsequent renders',
1524 + () => {
1525 + function App(props) {
1526 + /* eslint-disable no-unused-vars */
1527 + if (props.update) {
1528 + useMemoHelper();
1529 + useImperativeHandleHelper();
1530 + } else {
1531 + useImperativeHandleHelper();
1532 + useMemoHelper();
1533 + }
1534 + // This should not appear in the warning message because it occurs after the first mismatch
1535 + useRefHelper();
1536 + return null;
1537 + /* eslint-enable no-unused-vars */
1538 + }
1539 + let root = ReactTestRenderer.create(<App update={false} />);
1540 + expect(() => {
1541 + try {
1542 + root.update(<App update={true} />);
1543 + } catch (error) {
1544 + // Swapping certain types of hooks will cause runtime errors.
1545 + // This is okay as far as this test is concerned.
1546 + // We just want to verify that warnings are always logged.
1547 + }
1548 + }).toWarnDev([
1549 + 'Warning: React has detected a change in the order of Hooks called by App. ' +
1550 + 'This will lead to bugs and errors if not fixed. For more information, ' +
1551 + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
1552 + ' Previous render Next render\n' +
1553 + ' ------------------------------------------------------\n' +
1554 + `1. ${formatHookNamesToMatchErrorMessage(
1555 + 'ImperativeHandle',
1556 + 'Memo',
1557 + )}\n` +
1558 + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' +
1559 + ' in App (at **)',
1560 + ]);
1561 +
1562 + // further warnings for this component are silenced
1563 + root.update(<App update={false} />);
1564 + },
1565 + );
1566 +
1567 + it('detects a bad hook order even if the component throws', () => {
1568 + const {useState, useReducer} = React;
1569 + function useCustomHook() {
1570 + useState(0);
1571 }
1393 - return null;
1394 - /* eslint-enable no-unused-vars */
1395 - }
1396 - let root = ReactTestRenderer.create(<App flip={false} />);
1397 - expect(() => {
1398 - expect(() => root.update(<App flip={true} />)).toThrow('custom error');
1399 - }).toWarnDev([
1400 - 'Warning: React has detected a change in the order of Hooks called by App. ' +
1401 - 'This will lead to bugs and errors if not fixed. For more information, ' +
1402 - 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
1403 - ' Previous render Next render\n' +
1404 - ' -------------------------------\n' +
1405 - '1. useReducer useState\n' +
1406 - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^',
1407 - ]);
1572 + function App(props) {
1573 + /* eslint-disable no-unused-vars */
1574 + if (props.update) {
1575 + useCustomHook();
1576 + useReducer((s, a) => a, 0);
1577 + throw new Error('custom error');
1578 + } else {
1579 + useReducer((s, a) => a, 0);
1580 + useCustomHook();
1581 + }
1582 + return null;
1583 + /* eslint-enable no-unused-vars */
1584 + }
1585 + let root = ReactTestRenderer.create(<App update={false} />);
1586 + expect(() => {
1587 + expect(() => root.update(<App update={true} />)).toThrow(
1588 + 'custom error',
1589 + );
1590 + }).toWarnDev([
1591 + 'Warning: React has detected a change in the order of Hooks called by App. ' +
1592 + 'This will lead to bugs and errors if not fixed. For more information, ' +
1593 + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
1594 + ' Previous render Next render\n' +
1595 + ' ------------------------------------------------------\n' +
1596 + '1. useReducer useState\n' +
1597 + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
1598 + ]);
1599 + });
1600 });
1601
1602 // Regression test for #14674
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+27 -4
@@ -1739,8 +1739,20 @@ describe('ReactHooksWithNoopRenderer', () => {
1739
1740 ReactNoop.render(<App loadC={true} />);
1741 expect(() => {
1742 - expect(ReactNoop).toFlushAndYield(['A: 2, B: 3, C: 0']);
1743 - }).toThrow('Rendered more hooks than during the previous render');
1742 + expect(() => {
1743 + expect(ReactNoop).toFlushAndYield(['A: 2, B: 3, C: 0']);
1744 + }).toThrow('Rendered more hooks than during the previous render');
1745 + }).toWarnDev([
1746 + 'Warning: React has detected a change in the order of Hooks called by App. ' +
1747 + 'This will lead to bugs and errors if not fixed. For more information, ' +
1748 + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
1749 + ' Previous render Next render\n' +
1750 + ' ------------------------------------------------------\n' +
1751 + '1. useState useState\n' +
1752 + '2. useState useState\n' +
1753 + '3. undefined useState\n' +
1754 + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
1755 + ]);
1756
1757 // Uncomment if/when we support this again
1758 // expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 0')]);
@@ -1818,8 +1830,19 @@ describe('ReactHooksWithNoopRenderer', () => {
1830
1831 ReactNoop.render(<App showMore={true} />);
1832 expect(() => {
1821 - expect(ReactNoop).toFlushAndYield([]);
1822 - }).toThrow('Rendered more hooks than during the previous render');
1833 + expect(() => {
1834 + expect(ReactNoop).toFlushAndYield([]);
1835 + }).toThrow('Rendered more hooks than during the previous render');
1836 + }).toWarnDev([
1837 + 'Warning: React has detected a change in the order of Hooks called by App. ' +
1838 + 'This will lead to bugs and errors if not fixed. For more information, ' +
1839 + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
1840 + ' Previous render Next render\n' +
1841 + ' ------------------------------------------------------\n' +
1842 + '1. useEffect useEffect\n' +
1843 + '2. undefined useEffect\n' +
1844 + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
1845 + ]);
1846
1847 // Uncomment if/when we support this again
1848 // ReactNoop.flushPassiveEffects();