From 58da7330ea25ead450a8ee3ec3e83b742f2d0a9e Mon Sep 17 00:00:00 2001 From: Jessica Date: Wed, 6 Feb 2019 18:30:44 +0900 Subject: [PATCH 1/4] Update react-dom, react-test-renderer to 16.8.0 --- types/react-dom/index.d.ts | 3 ++- types/react-dom/react-dom-tests.tsx | 14 ++++++++++++++ types/react-dom/test-utils/index.d.ts | 14 ++++++++++++++ types/react-dom/tsconfig.json | 2 +- types/react-test-renderer/index.d.ts | 17 ++++++++++++++++- .../react-test-renderer-tests.ts | 11 +++++++++-- 6 files changed, 56 insertions(+), 5 deletions(-) diff --git a/types/react-dom/index.d.ts b/types/react-dom/index.d.ts index 518c0c617c..a6ac5c9d11 100644 --- a/types/react-dom/index.d.ts +++ b/types/react-dom/index.d.ts @@ -1,10 +1,11 @@ -// Type definitions for React (react-dom) 16.0 +// Type definitions for React (react-dom) 16.8 // Project: http://facebook.github.io/react/ // Definitions by: Asana // AssureSign // Microsoft // MartynasZilinskas // Josh Rutherford +// Jessica Franco // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped // TypeScript Version: 2.8 diff --git a/types/react-dom/react-dom-tests.tsx b/types/react-dom/react-dom-tests.tsx index c7ccb0052d..e81c500f9c 100644 --- a/types/react-dom/react-dom-tests.tsx +++ b/types/react-dom/react-dom-tests.tsx @@ -178,4 +178,18 @@ describe('React dom test utils', () => { shallowRenderer.getRenderOutput(); }); }); + + describe('act', () => { + it('accepts a sync callback that is void', () => { + ReactTestUtils.act(() => {}); + }); + it('rejects an async callback even if void', () => { + // $ExpectError + ReactTestUtils.act(async () => {}); + }); + it('rejects a callback that returns null', () => { + // $ExpectError + ReactTestUtils.act(() => null); + }); + }); }); diff --git a/types/react-dom/test-utils/index.d.ts b/types/react-dom/test-utils/index.d.ts index f60d088515..f1eae65df6 100644 --- a/types/react-dom/test-utils/index.d.ts +++ b/types/react-dom/test-utils/index.d.ts @@ -278,3 +278,17 @@ export function findRenderedComponentWithType, C extend * Call this in your tests to create a shallow renderer. */ export function createRenderer(): ShallowRenderer; + +/** + * Wrap any code rendering and triggering updates to your components into `act()` calls. + * + * Ensures that the behavior in your tests matches what happens in the browser + * more closely by executing pending `useEffect`s before returning. This also + * reduces the amount of re-renders done. + * + * @param callback A synchronous, void callback that will execute as a single, complete React commit. + * + * @see https://reactjs.org/blog/2019/02/06/react-v16.8.0.html#testing-hooks + */ +// the "void | undefined" is here to forbid any sneaky "Promise" returns. +export function act(callback: () => void | undefined): void; diff --git a/types/react-dom/tsconfig.json b/types/react-dom/tsconfig.json index f63325026f..6d7d943c8b 100644 --- a/types/react-dom/tsconfig.json +++ b/types/react-dom/tsconfig.json @@ -14,7 +14,7 @@ ], "noImplicitAny": true, "noImplicitThis": true, - "strictNullChecks": false, + "strictNullChecks": true, "strictFunctionTypes": true, "baseUrl": "../", "typeRoots": [ diff --git a/types/react-test-renderer/index.d.ts b/types/react-test-renderer/index.d.ts index c147d5c190..040ec3a513 100644 --- a/types/react-test-renderer/index.d.ts +++ b/types/react-test-renderer/index.d.ts @@ -1,9 +1,10 @@ -// Type definitions for react-test-renderer 16.0 +// Type definitions for react-test-renderer 16.8 // Project: https://facebook.github.io/react/ // Definitions by: Arvitaly // Lochbrunner // John Reilly // John Gozde +// Jessica Franco // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped // TypeScript Version: 2.8 @@ -50,3 +51,17 @@ export interface TestRendererOptions { createNodeMock(element: ReactElement): any; } export function create(nextElement: ReactElement, options?: TestRendererOptions): ReactTestRenderer; + +/** + * Wrap any code rendering and triggering updates to your components into `act()` calls. + * + * Ensures that the behavior in your tests matches what happens in the browser + * more closely by executing pending `useEffect`s before returning. This also + * reduces the amount of re-renders done. + * + * @param callback A synchronous, void callback that will execute as a single, complete React commit. + * + * @see https://reactjs.org/blog/2019/02/06/react-v16.8.0.html#testing-hooks + */ +// the "void | undefined" is here to forbid any sneaky "Promise" returns. +export function act(callback: () => void | undefined): void; diff --git a/types/react-test-renderer/react-test-renderer-tests.ts b/types/react-test-renderer/react-test-renderer-tests.ts index 83dc510e2b..b7e23caf25 100644 --- a/types/react-test-renderer/react-test-renderer-tests.ts +++ b/types/react-test-renderer/react-test-renderer-tests.ts @@ -1,5 +1,5 @@ -import * as React from "react"; -import { create, ReactTestInstance } from "react-test-renderer"; +import React = require("react"); +import { act, create, ReactTestInstance } from "react-test-renderer"; import { createRenderer } from 'react-test-renderer/shallow'; class TestComponent extends React.Component { } @@ -66,3 +66,10 @@ const shallowRenderer = createRenderer(); shallowRenderer.render(component); shallowRenderer.getRenderOutput(); shallowRenderer.getMountedInstance(); + +// Only synchronous, void callbacks are acceptable for act() +act(() => {}); +// $ExpectError +act(async () => {}); +// $ExpectError +act(() => null); From 1755edd5fc18384333ed91db9b4d60098ee5e32c Mon Sep 17 00:00:00 2001 From: Jessica Date: Wed, 6 Feb 2019 18:39:55 +0900 Subject: [PATCH 2/4] Forbid returning non-void from the useEffect destructor too There is a runtime warning. --- types/react/index.d.ts | 16 +++++++++------- types/react/test/hooks.tsx | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/types/react/index.d.ts b/types/react/index.d.ts index 929a698775..9e46bc5ca9 100644 --- a/types/react/index.d.ts +++ b/types/react/index.d.ts @@ -783,11 +783,9 @@ declare namespace React { // TODO (TypeScript 3.0): ReadonlyArray type DependencyList = ReadonlyArray; - // NOTE: Currently, in alpha.0, the effect callbacks are actually allowed to return anything, - // but functions are treated specially. The next version published with hooks will warn if you actually - // return anything besides `void` or a callback. Async effects need to call an async function inside - // them. - type EffectCallback = () => (void | (() => void)); + // NOTE: callbacks are _only_ allowed to return either void, or a destructor. + // The destructor is itself only allowed to return void. + type EffectCallback = () => (void | (() => void | undefined)); interface MutableRefObject { current: T; @@ -853,12 +851,16 @@ declare namespace React { * @version 16.8.0 * @see https://reactjs.org/docs/hooks-reference.html#usereducer */ + // I'm not sure if I keep this 2-ary or if I make it (2,3)-ary; it's currently (2,3)-ary. // The Flow types do have an overload for 3-ary invocation with undefined initializer. - // NOTE: the documentation or any alphas aren't updated, this is current for master. - // NOTE 2: without the ReducerState indirection, TypeScript would reduce S to be the most common + + // NOTE: without the ReducerState indirection, TypeScript would reduce S to be the most common // supertype between the reducer's return type and the initialState (or the initializer's return type), // which would prevent autocompletion from ever working. + + // TODO: double-check if this weird overload logic is necessary. It is possible it's either a bug + // in older versions, or a regression in newer versions of the typescript completion service. function useReducer>( reducer: R, initialState: ReducerState, diff --git a/types/react/test/hooks.tsx b/types/react/test/hooks.tsx index 12fcc28222..26ff634e89 100644 --- a/types/react/test/hooks.tsx +++ b/types/react/test/hooks.tsx @@ -120,6 +120,23 @@ function useEveryHook(ref: React.Ref<{ id: number }>|undefined): () => boolean { setState(reducerState.age); }, []); + // effects are only allowed to either be actually void or return actually void functions + React.useEffect(() => () => {}); + // indistinguishable + React.useEffect(() => () => undefined); + // $ExpectError + React.useEffect(() => null); + // $ExpectError + React.useEffect(() => Math.random() ? null : undefined); + // $ExpectError + React.useEffect(() => () => null); + // $ExpectError + React.useEffect(() => () => Math.random() ? null : undefined); + // $ExpectError + React.useEffect(() => async () => {}); + // $ExpectError + React.useEffect(async () => () => {}); + React.useDebugValue(id, value => value.toFixed()); React.useDebugValue(id); From 8af624afbe50926ee476cbad993b80da2ce07802 Mon Sep 17 00:00:00 2001 From: Jessica Date: Wed, 6 Feb 2019 19:07:43 +0900 Subject: [PATCH 3/4] Use contrivances to present something close to act()'s real return type Make it harder to accidentally use it in contexts where a Promise may be expected. --- types/react-dom/react-dom-tests.tsx | 5 +++++ types/react-dom/test-utils/index.d.ts | 11 ++++++++++- types/react-test-renderer/index.d.ts | 11 ++++++++++- .../react-test-renderer/react-test-renderer-tests.ts | 2 ++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/types/react-dom/react-dom-tests.tsx b/types/react-dom/react-dom-tests.tsx index e81c500f9c..46274e3ffd 100644 --- a/types/react-dom/react-dom-tests.tsx +++ b/types/react-dom/react-dom-tests.tsx @@ -191,5 +191,10 @@ describe('React dom test utils', () => { // $ExpectError ReactTestUtils.act(() => null); }); + it('returns a Promise-like that errors out on use', () => { + const result = ReactTestUtils.act(() => {}); + // $ExpectError + Promise.resolve(result); + }); }); }); diff --git a/types/react-dom/test-utils/index.d.ts b/types/react-dom/test-utils/index.d.ts index f1eae65df6..fb98181847 100644 --- a/types/react-dom/test-utils/index.d.ts +++ b/types/react-dom/test-utils/index.d.ts @@ -291,4 +291,13 @@ export function createRenderer(): ShallowRenderer; * @see https://reactjs.org/blog/2019/02/06/react-v16.8.0.html#testing-hooks */ // the "void | undefined" is here to forbid any sneaky "Promise" returns. -export function act(callback: () => void | undefined): void; +// the actual return value is always a "DebugPromiseLike", +// but having an "| {}" makes it harder to accidentally use. +export function act(callback: () => void | undefined): DebugPromiseLike | {}; + +// Intentionally doesn't extend PromiseLike. +// Ideally this should be as hard to accidentally use as possible. +interface DebugPromiseLike { + // the actual then() in here is 0-ary, but that doesn't count as a PromiseLike. + then(onfulfilled: (value: never) => never, onrejected: (reason: never) => never): never; +} diff --git a/types/react-test-renderer/index.d.ts b/types/react-test-renderer/index.d.ts index 040ec3a513..34ddb2502c 100644 --- a/types/react-test-renderer/index.d.ts +++ b/types/react-test-renderer/index.d.ts @@ -64,4 +64,13 @@ export function create(nextElement: ReactElement, options?: TestRendererOpt * @see https://reactjs.org/blog/2019/02/06/react-v16.8.0.html#testing-hooks */ // the "void | undefined" is here to forbid any sneaky "Promise" returns. -export function act(callback: () => void | undefined): void; +// the actual return value is always a "DebugPromiseLike", +// but having an "| {}" makes it harder to accidentally use. +export function act(callback: () => void | undefined): DebugPromiseLike | {}; + +// Intentionally doesn't extend PromiseLike. +// Ideally this should be as hard to accidentally use as possible. +interface DebugPromiseLike { + // the actual then() in here is 0-ary, but that doesn't count as a PromiseLike. + then(onfulfilled: (value: never) => never, onrejected: (reason: never) => never): never; +} diff --git a/types/react-test-renderer/react-test-renderer-tests.ts b/types/react-test-renderer/react-test-renderer-tests.ts index b7e23caf25..48d861cf62 100644 --- a/types/react-test-renderer/react-test-renderer-tests.ts +++ b/types/react-test-renderer/react-test-renderer-tests.ts @@ -73,3 +73,5 @@ act(() => {}); act(async () => {}); // $ExpectError act(() => null); +// $ExpectError +Promise.resolve(act(() => {})); From 987f88cc20bd60a60a23640ab76f52fe1f7fca08 Mon Sep 17 00:00:00 2001 From: Jessica Date: Wed, 6 Feb 2019 19:53:52 +0900 Subject: [PATCH 4/4] Add missing export annotations --- types/react-dom/test-utils/index.d.ts | 2 +- types/react-test-renderer/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/types/react-dom/test-utils/index.d.ts b/types/react-dom/test-utils/index.d.ts index fb98181847..8730dc9447 100644 --- a/types/react-dom/test-utils/index.d.ts +++ b/types/react-dom/test-utils/index.d.ts @@ -297,7 +297,7 @@ export function act(callback: () => void | undefined): DebugPromiseLike | {}; // Intentionally doesn't extend PromiseLike. // Ideally this should be as hard to accidentally use as possible. -interface DebugPromiseLike { +export interface DebugPromiseLike { // the actual then() in here is 0-ary, but that doesn't count as a PromiseLike. then(onfulfilled: (value: never) => never, onrejected: (reason: never) => never): never; } diff --git a/types/react-test-renderer/index.d.ts b/types/react-test-renderer/index.d.ts index 34ddb2502c..abdae1c908 100644 --- a/types/react-test-renderer/index.d.ts +++ b/types/react-test-renderer/index.d.ts @@ -70,7 +70,7 @@ export function act(callback: () => void | undefined): DebugPromiseLike | {}; // Intentionally doesn't extend PromiseLike. // Ideally this should be as hard to accidentally use as possible. -interface DebugPromiseLike { +export interface DebugPromiseLike { // the actual then() in here is 0-ary, but that doesn't count as a PromiseLike. then(onfulfilled: (value: never) => never, onrejected: (reason: never) => never): never; }