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..46274e3ffd 100644 --- a/types/react-dom/react-dom-tests.tsx +++ b/types/react-dom/react-dom-tests.tsx @@ -178,4 +178,23 @@ 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); + }); + 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 f60d088515..8730dc9447 100644 --- a/types/react-dom/test-utils/index.d.ts +++ b/types/react-dom/test-utils/index.d.ts @@ -278,3 +278,26 @@ 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. +// 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. +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-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..abdae1c908 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,26 @@ 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. +// 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. +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/react-test-renderer-tests.ts b/types/react-test-renderer/react-test-renderer-tests.ts index 83dc510e2b..48d861cf62 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,12 @@ 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); +// $ExpectError +Promise.resolve(act(() => {})); 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);