@samitouri / QOS-React-2 / commits / 66eb293742

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