Restrict effect return type to a function or nothing (#14119)
* Restrict effect return type to a function or nothing We already warn in dev if the wrong type is returned. This updates the Flow type. * Restrict return type further * Assume Effect hook returns either a function or undefined * Tweak warning message
Andrew Clark committed
Jan 31, 2019 at 10:11 UTC
66eb29374239945c3a512a88aa2480637f62e5cc
6 files changed
+102
-65
packages/react-debug-tools/src/ReactDebugHooks.js
+2
-2
@@ -147,7 +147,7 @@ function useRef<T>(initialValue: T): {current: T} {
147
}
148
149
function useLayoutEffect(
150
- create: () => mixed,
150
+ create: () => (() => void) | void,
151
inputs: Array<mixed> | void | null,
152
): void {
153
nextHook();
@@ -159,7 +159,7 @@ function useLayoutEffect(
159
}
160
161
function useEffect(
162
- create: () => mixed,
162
+ create: () => (() => void) | void,
163
inputs: Array<mixed> | void | null,
164
): void {
165
nextHook();
packages/react-dom/src/server/ReactPartialRendererHooks.js
+2
-2
@@ -375,8 +375,8 @@ function useRef<T>(initialValue: T): {current: T} {
375
}
376
377
export function useLayoutEffect(
378
- create: () => mixed,
379
- deps: Array<mixed> | void | null,
378
+ create: () => (() => void) | void,
379
+ inputs: Array<mixed> | void | null,
380
) {
381
if (__DEV__) {
382
currentHookNameInDev = 'useLayoutEffect';
packages/react-reconciler/src/ReactFiberCommitWork.js
+35
-28
@@ -320,42 +320,49 @@ function commitHookEffectList(
320
if ((effect.tag & unmountTag) !== NoHookEffect) {
321
// Unmount
322
const destroy = effect.destroy;
323
- effect.destroy = null;
324
- if (destroy !== null) {
323
+ effect.destroy = undefined;
324
+ if (destroy !== undefined) {
325
destroy();
326
}
327
}
328
if ((effect.tag & mountTag) !== NoHookEffect) {
329
// Mount
330
const create = effect.create;
331
- let destroy = create();
332
- if (typeof destroy !== 'function') {
333
- if (__DEV__) {
334
- if (destroy !== null && destroy !== undefined) {
335
- warningWithoutStack(
336
- false,
337
- 'useEffect function must return a cleanup function or ' +
338
- 'nothing.%s%s',
339
- typeof destroy.then === 'function'
340
- ? '\n\nIt looks like you wrote useEffect(async () => ...) or returned a Promise. ' +
341
- 'Instead, you may write an async function separately ' +
342
- 'and then call it from inside the effect:\n\n' +
343
- 'async function fetchComment(commentId) {\n' +
344
- ' // You can await here\n' +
345
- '}\n\n' +
346
- 'useEffect(() => {\n' +
347
- ' fetchComment(commentId);\n' +
348
- '}, [commentId]);\n\n' +
349
- 'In the future, React will provide a more idiomatic solution for data fetching ' +
350
- "that doesn't involve writing effects manually."
351
- : '',
352
- getStackByFiberInDevAndProd(finishedWork),
353
- );
331
+ effect.destroy = create();
332
+
333
+ if (__DEV__) {
334
+ const destroy = effect.destroy;
335
+ if (destroy !== undefined && typeof destroy !== 'function') {
336
+ let addendum;
337
+ if (destroy === null) {
338
+ addendum =
339
+ ' You returned null. If your effect does not require clean ' +
340
+ 'up, return undefined (or nothing).';
341
+ } else if (typeof destroy.then === 'function') {
342
+ addendum =
343
+ '\n\nIt looks like you wrote useEffect(async () => ...) or returned a Promise. ' +
344
+ 'Instead, you may write an async function separately ' +
345
+ 'and then call it from inside the effect:\n\n' +
346
+ 'async function fetchComment(commentId) {\n' +
347
+ ' // You can await here\n' +
348
+ '}\n\n' +
349
+ 'useEffect(() => {\n' +
350
+ ' fetchComment(commentId);\n' +
351
+ '}, [commentId]);\n\n' +
352
+ 'In the future, React will provide a more idiomatic solution for data fetching ' +
353
+ "that doesn't involve writing effects manually.";
354
+ } else {
355
+ addendum = ' You returned: ' + destroy;
356
}
357
+ warningWithoutStack(
358
+ false,
359
+ 'An Effect function must not return anything besides a function, ' +
360
+ 'which is used for clean-up.%s%s',
361
+ addendum,
362
+ getStackByFiberInDevAndProd(finishedWork),
363
+ );
364
}
356
- destroy = null;
365
}
358
- effect.destroy = destroy;
366
}
367
effect = effect.next;
368
} while (effect !== firstEffect);
@@ -696,7 +703,7 @@ function commitUnmount(current: Fiber): void {
703
let effect = firstEffect;
704
do {
705
const destroy = effect.destroy;
699
- if (destroy !== null) {
706
+ if (destroy !== undefined) {
707
safelyCallDestroy(current, destroy);
708
}
709
effect = effect.next;
packages/react-reconciler/src/ReactFiberHooks.js
+39
-19
@@ -59,8 +59,14 @@ export type Dispatcher = {
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,
62
+ useEffect(
63
+ create: () => (() => void) | void,
64
+ deps: Array<mixed> | void | null,
65
+ ): void,
66
+ useLayoutEffect(
67
+ create: () => (() => void) | void,
68
+ deps: Array<mixed> | void | null,
69
+ ): void,
70
useCallback<T>(callback: T, deps: Array<mixed> | void | null): T,
71
useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T,
72
useImperativeHandle<T>(
@@ -119,8 +125,8 @@ type HookDev = Hook & {
125
126
type Effect = {
127
tag: HookEffectTag,
122
- create: () => mixed,
123
- destroy: (() => mixed) | null,
128
+ create: () => (() => void) | void,
129
+ destroy: (() => void) | void,
130
deps: Array<mixed> | null,
131
next: Effect,
132
};
@@ -780,13 +786,13 @@ function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
786
const hook = mountWorkInProgressHook();
787
const nextDeps = deps === undefined ? null : deps;
788
sideEffectTag |= fiberEffectTag;
783
- hook.memoizedState = pushEffect(hookEffectTag, create, null, nextDeps);
789
+ hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
790
}
791
792
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
793
const hook = updateWorkInProgressHook();
794
const nextDeps = deps === undefined ? null : deps;
789
- let destroy = null;
795
+ let destroy = undefined;
796
797
if (currentHook !== null) {
798
const prevEffect = currentHook.memoizedState;
@@ -805,7 +811,7 @@ function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
811
}
812
813
function mountEffect(
808
- create: () => mixed,
814
+ create: () => (() => void) | void,
815
deps: Array<mixed> | void | null,
816
): void {
817
return mountEffectImpl(
@@ -817,7 +823,7 @@ function mountEffect(
823
}
824
825
function updateEffect(
820
- create: () => mixed,
826
+ create: () => (() => void) | void,
827
deps: Array<mixed> | void | null,
828
): void {
829
return updateEffectImpl(
@@ -829,7 +835,7 @@ function updateEffect(
835
}
836
837
function mountLayoutEffect(
832
- create: () => mixed,
838
+ create: () => (() => void) | void,
839
deps: Array<mixed> | void | null,
840
): void {
841
return mountEffectImpl(
@@ -841,7 +847,7 @@ function mountLayoutEffect(
847
}
848
849
function updateLayoutEffect(
844
- create: () => mixed,
850
+ create: () => (() => void) | void,
851
deps: Array<mixed> | void | null,
852
): void {
853
return updateEffectImpl(
@@ -860,7 +866,9 @@ function imperativeHandleEffect<T>(
866
const refCallback = ref;
867
const inst = create();
868
refCallback(inst);
863
- return () => refCallback(null);
869
+ return () => {
870
+ refCallback(null);
871
+ };
872
} else if (ref !== null && ref !== undefined) {
873
const refObject = ref;
874
if (__DEV__) {
@@ -1205,7 +1213,10 @@ if (__DEV__) {
1213
currentHookNameInDev = 'useContext';
1214
return mountContext(context, observedBits);
1215
},
1208
- useEffect(create: () => mixed, deps: Array<mixed> | void | null): void {
1216
+ useEffect(
1217
+ create: () => (() => void) | void,
1218
+ deps: Array<mixed> | void | null,
1219
+ ): void {
1220
currentHookNameInDev = 'useEffect';
1221
return mountEffect(create, deps);
1222
},
@@ -1218,7 +1229,7 @@ if (__DEV__) {
1229
return mountImperativeHandle(ref, create, deps);
1230
},
1231
useLayoutEffect(
1221
- create: () => mixed,
1232
+ create: () => (() => void) | void,
1233
deps: Array<mixed> | void | null,
1234
): void {
1235
currentHookNameInDev = 'useLayoutEffect';
@@ -1289,7 +1300,10 @@ if (__DEV__) {
1300
currentHookNameInDev = 'useContext';
1301
return updateContext(context, observedBits);
1302
},
1292
- useEffect(create: () => mixed, deps: Array<mixed> | void | null): void {
1303
+ useEffect(
1304
+ create: () => (() => void) | void,
1305
+ deps: Array<mixed> | void | null,
1306
+ ): void {
1307
currentHookNameInDev = 'useEffect';
1308
return updateEffect(create, deps);
1309
},
@@ -1302,7 +1316,7 @@ if (__DEV__) {
1316
return updateImperativeHandle(ref, create, deps);
1317
},
1318
useLayoutEffect(
1305
- create: () => mixed,
1319
+ create: () => (() => void) | void,
1320
deps: Array<mixed> | void | null,
1321
): void {
1322
currentHookNameInDev = 'useLayoutEffect';
@@ -1376,7 +1390,10 @@ if (__DEV__) {
1390
warnInvalidHookAccess();
1391
return mountContext(context, observedBits);
1392
},
1379
- useEffect(create: () => mixed, deps: Array<mixed> | void | null): void {
1393
+ useEffect(
1394
+ create: () => (() => void) | void,
1395
+ deps: Array<mixed> | void | null,
1396
+ ): void {
1397
currentHookNameInDev = 'useEffect';
1398
warnInvalidHookAccess();
1399
return mountEffect(create, deps);
@@ -1391,7 +1408,7 @@ if (__DEV__) {
1408
return mountImperativeHandle(ref, create, deps);
1409
},
1410
useLayoutEffect(
1394
- create: () => mixed,
1411
+ create: () => (() => void) | void,
1412
deps: Array<mixed> | void | null,
1413
): void {
1414
currentHookNameInDev = 'useLayoutEffect';
@@ -1471,7 +1488,10 @@ if (__DEV__) {
1488
warnInvalidHookAccess();
1489
return updateContext(context, observedBits);
1490
},
1474
- useEffect(create: () => mixed, deps: Array<mixed> | void | null): void {
1491
+ useEffect(
1492
+ create: () => (() => void) | void,
1493
+ deps: Array<mixed> | void | null,
1494
+ ): void {
1495
currentHookNameInDev = 'useEffect';
1496
warnInvalidHookAccess();
1497
return updateEffect(create, deps);
@@ -1486,7 +1506,7 @@ if (__DEV__) {
1506
return updateImperativeHandle(ref, create, deps);
1507
},
1508
useLayoutEffect(
1489
- create: () => mixed,
1509
+ create: () => (() => void) | void,
1510
deps: Array<mixed> | void | null,
1511
): void {
1512
currentHookNameInDev = 'useLayoutEffect';
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+22
-12
@@ -542,30 +542,40 @@ describe('ReactHooks', () => {
542
]);
543
});
544
545
- it('warns for bad useEffect return values', () => {
545
+ it('assumes useEffect clean-up function is either a function or undefined', () => {
546
const {useLayoutEffect} = React;
547
+
548
function App(props) {
549
useLayoutEffect(() => {
550
return props.return;
551
});
552
return null;
553
}
553
- let root;
554
555
- expect(() => {
556
- root = ReactTestRenderer.create(<App return={17} />);
557
- }).toWarnDev([
558
- 'Warning: useEffect function must return a cleanup function or ' +
559
- 'nothing.\n' +
560
- ' in App (at **)',
555
+ const root1 = ReactTestRenderer.create(null);
556
+ expect(() => root1.update(<App return={17} />)).toWarnDev([
557
+ 'Warning: An Effect function must not return anything besides a ' +
558
+ 'function, which is used for clean-up. You returned: 17',
559
]);
560
563
- expect(() => {
564
- root.update(<App return={Promise.resolve()} />);
565
- }).toWarnDev([
566
- 'Warning: useEffect function must return a cleanup function or nothing.\n\n' +
561
+ const root2 = ReactTestRenderer.create(null);
562
+ expect(() => root2.update(<App return={null} />)).toWarnDev([
563
+ 'Warning: An Effect function must not return anything besides a ' +
564
+ 'function, which is used for clean-up. You returned null. If your ' +
565
+ 'effect does not require clean up, return undefined (or nothing).',
566
+ ]);
567
+
568
+ const root3 = ReactTestRenderer.create(null);
569
+ expect(() => root3.update(<App return={Promise.resolve()} />)).toWarnDev([
570
+ 'Warning: An Effect function must not return anything besides a ' +
571
+ 'function, which is used for clean-up.\n\n' +
572
'It looks like you wrote useEffect(async () => ...) or returned a Promise.',
573
]);
574
+
575
+ // Error on unmount because React assumes the value is a function
576
+ expect(() => {
577
+ root3.update(null);
578
+ }).toThrow('is not a function');
579
});
580
581
it('warns for bad useImperativeHandle first arg', () => {
packages/react/src/ReactHooks.js
+2
-2
@@ -84,7 +84,7 @@ export function useRef<T>(initialValue: T): {current: T} {
84
}
85
86
export function useEffect(
87
- create: () => mixed,
87
+ create: () => (() => void) | void,
88
inputs: Array<mixed> | void | null,
89
) {
90
const dispatcher = resolveDispatcher();
@@ -92,7 +92,7 @@ export function useEffect(
92
}
93
94
export function useLayoutEffect(
95
- create: () => mixed,
95
+ create: () => (() => void) | void,
96
inputs: Array<mixed> | void | null,
97
) {
98
const dispatcher = resolveDispatcher();