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();