diff --git a/README.md b/README.md index 4674bda3..bc2d9bcd 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ + - [The problem](#the-problem) - [The solution](#the-solution) - [When to use this library](#when-to-use-this-library) @@ -141,17 +142,22 @@ npm install --save-dev @testing-library/react-hooks ### Peer Dependencies `react-hooks-testing-library` does not come bundled with a version of -[`react`](https://www.npmjs.com/package/react) or -[`react-test-renderer`](https://www.npmjs.com/package/react-test-renderer) to allow you to install -the specific version you want to test against. Generally, the installed versions for `react` and -`react-test-renderer` should have matching versions: +[`react`](https://www.npmjs.com/package/react) to allow you to install the specific version you want +to test against. It also does not come installed with a specific renderer, we currently support +[`react-test-renderer`](https://www.npmjs.com/package/react-test-renderer) and +[`react-dom`](https://www.npmjs.com/package/react-dom). You only need to install one of them, +however, if you do have both installed, we will use `react-test-renderer` as the default. For more +information see the [installation docs](https://react-hooks-testing-library.com/#installation). +Generally, the installed versions for `react` and the selected renderer should have matching +versions: ```sh npm install react@^16.9.0 npm install --save-dev react-test-renderer@^16.9.0 ``` -> **NOTE: The minimum supported version of `react` and `react-test-renderer` is `^16.9.0`.** +> **NOTE: The minimum supported version of `react`, `react-test-renderer` and `react-dom` is +> `^16.9.0`.** ## API diff --git a/docs/api-reference.md b/docs/api-reference.md index fa7daf4b..b6d946aa 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -11,16 +11,14 @@ route: '/reference/api' - [`act`](/reference/api#act) - [`cleanup`](/reference/api#cleanup) - [`addCleanup`](/reference/api#addcleanup) +- [`removeCleanup`](/reference/api#removecleanup) --- ## `renderHook` -```js -function renderHook( - callback: function(props?: any): any, - options?: RenderHookOptions -): RenderHookResult +```ts +function renderHook(callback: (props?: any) => any, options?: RenderHookOptions): RenderHookResult ``` Renders a test component that will call the provided `callback`, including any hooks it calls, every @@ -61,7 +59,7 @@ The `renderHook` function returns an object that has the following properties: ### `result` -```js +```ts { all: Array current: any, @@ -77,7 +75,7 @@ returned at the time. ### `rerender` -```js +```ts function rerender(newProps?: any): void ``` @@ -86,13 +84,27 @@ passed, they will replace the `callback` function's `initialProps` for subsequen ### `unmount` -```js +```ts function unmount(): void ``` A function to unmount the test component. This is commonly used to trigger cleanup effects for `useEffect` hooks. +### `hydrate` + +```ts +function hydrate(): void +``` + +> This is only used when using the `server` module. See [SSR](/usage/ssr) for more information on +> server-side rendering your hooks. + +A function to hydrate a server rendered component into the DOM. This is required before you can +interact with the hook, whether that is an `act` or `rerender` call. Effects created using +`useEffect` or `useLayoutEffect` are also not run on server rendered hooks until `hydrate` is +called. + ### `...asyncUtils` Utilities to assist with testing asynchronous behaviour. See the @@ -102,15 +114,15 @@ Utilities to assist with testing asynchronous behaviour. See the ## `act` -This is the same [`act` function](https://reactjs.org/docs/test-utils.html#act) that is exported by -`react-test-renderer`. +This is the same [`act` function](https://reactjs.org/docs/test-utils.html#act) function that is +exported from your [chosen renderer](/installation#renderer). --- ## `cleanup` -```js -function cleanup: Promise +```ts +function cleanup(): Promise ``` Unmounts any rendered hooks rendered with `renderHook`, ensuring all effects have been flushed. Any @@ -142,7 +154,8 @@ module.exports = { ``` Alternatively, you can change your test to import from `@testing-library/react-hooks/pure` instead -of the regular imports. +of the regular imports. This applys to any of our export methods documented in +[Rendering](/installation#being-specific). ```diff - import { renderHook, cleanup, act } from '@testing-library/react-hooks' @@ -156,8 +169,8 @@ variable to `true` before importing `@testing-library/react-hooks` will also dis ## `addCleanup` -```js -function addCleanup(callback: function(): void|Promise): function(): void +```ts +function addCleanup(callback: () => void | Promise): (): void ``` Add a callback to be called during [`cleanup`](/reference/api#cleanup), returning a function to @@ -173,8 +186,8 @@ be resolved before moving onto the next cleanup callback. ## `removeCleanup` -```js -function removeCleanup(callback: function(): void|Promise): void +```ts +function removeCleanup(callback: () => void | Promise): void ``` Removes a cleanup callback previously added with [`addCleanup`](/reference/api#addCleanup). Once @@ -187,10 +200,8 @@ removed, the provided callback will no longer execute as part of running ### `waitForNextUpdate` -```js -function waitForNextUpdate(options?: { - timeout?: number -}): Promise +```ts +function waitForNextUpdate(options?: { timeout?: number }): Promise ``` Returns a `Promise` that resolves the next time the hook renders, commonly when state is updated as @@ -202,12 +213,15 @@ The maximum amount of time in milliseconds (ms) to wait. By default, no timeout ### `waitFor` -```js -function waitFor(callback: function(): boolean|void, options?: { - interval?: number, - timeout?: number, - suppressErrors?: boolean -}): Promise +```ts +function waitFor( + callback: () => boolean | void, + options?: { + interval?: number + timeout?: number + suppressErrors?: boolean + } +): Promise ``` Returns a `Promise` that resolves if the provided callback executes without exception and returns a @@ -232,12 +246,15 @@ rejected. By default, errors are suppressed for this utility. ### `waitForValueToChange` -```js -function waitForValueToChange(selector: function(): any, options?: { - interval?: number, - timeout?: number, - suppressErrors?: boolean -}): Promise +```ts +function waitForValueToChange( + selector: () => any, + options?: { + interval?: number + timeout?: number + suppressErrors?: boolean + } +): Promise ``` Returns a `Promise` that resolves if the value returned from the provided selector changes. It is diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 00000000..33f17f25 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,99 @@ +--- +name: Installation +route: '/installation' +--- + +# Installation + +## Getting started + +This module is distributed via [npm](https://www.npmjs.com/) which is bundled with +[node](https://nodejs.org) and should be installed as one of your project's `devDependencies`: + +```sh +# if you're using npm +npm install --save-dev @testing-library/react-hooks +# if you're using yarn +yarn add --dev @testing-library/react-hooks +``` + +### Peer Dependencies + +`react-hooks-testing-library` does not come bundled with a version of +[`react`](https://www.npmjs.com/package/react) to allow you to install the specific version you want +to test against. It also does not come installed with a specific renderer, we currently support +[`react-test-renderer`](https://www.npmjs.com/package/react-test-renderer) and +[`react-dom`](https://www.npmjs.com/package/react-dom), you only need to install one of them. For +more information see [Renderer](/installation#renderer) + +```sh +npm install react@^16.9.0 +npm install --save-dev react-test-renderer@^16.9.0 +``` + +> **NOTE: The minimum supported version of `react`, `react-test-renderer` and `react-dom` is +> `^16.9.0`.** + +## Renderer + +When running tests, a renderer is required in order to render the React component we wrap around +your hook. We currently support two different renderers: + +- `react-test-renderer` +- `react-dom` + +When using standard import for this library (show below), we will attempt to auto-detect which +renderer you have installed and use it without needing any specific wiring up to make it happen. If +you have both installed in your project, and you use the standard import (see below) the library +will default to using `react-test-renderer`. + +> We use `react-test-renderer` by default as it enables hooks to be tested that are designed for +> either `react` or `react-native` and it is compatible with more test runners out-of-the-box as +> there is no DOM requirement to use it. + +The standard import looks like: + +```js +import { renderHook } from '@testing-library/react-hooks' +``` + +> Note: The auto detection function may not work if tests are bundles before execution (e.g. to be +> run in a browser) + +### Act + +Each render also provides a unique [`act` function](https://reactjs.org/docs/test-utils.html#act) +that cannot be used with other renderers. In order to simplify with `act `function you need to use, +we also export the correct one alongside the detected renderer for you: + +```js +import { renderHook, act } from '@testing-library/react-hooks' +``` + +## Being specific + +Auto-detection is great for simplifying setup and getting out of your way, but sometimes you do need +a little but more control. If a test needs requires a specific type of environment, the import can +be appended to force a specific renderer to be use. The supported environments are: + +- `dom` +- `native` +- `server` + +The imports for each type of renderer are as follows: + +```ts +import { renderHook, act } from '@testing-library/react-hooks' // will attempt to auto-detect + +import { renderHook, act } from '@testing-library/react-hooks/dom' // will use react-dom + +import { renderHook, act } from '@testing-library/react-hooks/native' // will use react-test-renderer + +import { renderHook, act } from '@testing-library/react-hooks/server' // will use react-dom/server +``` + +## Testing Framework + +In order to run tests, you will probably want to be using a test framework. If you have not already +got one, we recommend using [Jest](https://jestjs.io/), but this library should work without issues +with any of the alternatives. diff --git a/docs/introduction.md b/docs/introduction.md index 70d2f505..5c06f357 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -55,33 +55,3 @@ the results. 1. Your hook is defined alongside a component and is only used there 2. Your hook is easy to test by just testing the components using it - -## Installation - -This module is distributed via [npm](https://www.npmjs.com/) which is bundled with -[node](https://nodejs.org) and should be installed as one of your project's `devDependencies`: - -```sh -npm install --save-dev @testing-library/react-hooks -``` - -### Peer Dependencies - -`react-hooks-testing-library` does not come bundled with a version of -[`react`](https://www.npmjs.com/package/react) or -[`react-test-renderer`](https://www.npmjs.com/package/react-test-renderer) to allow you to install -the specific version you want to test against. Generally, the installed versions for `react` and -`react-test-renderer` should have matching versions: - -```sh -npm install react@^16.9.0 -npm install --save-dev react-test-renderer@^16.9.0 -``` - -> **NOTE: The minimum supported version of `react` and `react-test-renderer` is `^16.9.0`.** - -## Testing Framework - -In order to run tests, you will probably want to be using a test framework. If you have not already -got one, we recommend using [Jest](https://jestjs.io/), but this library should work without issues -with any of the alternatives. diff --git a/docs/usage/ssr-hooks.md b/docs/usage/ssr-hooks.md new file mode 100644 index 00000000..58a493c3 --- /dev/null +++ b/docs/usage/ssr-hooks.md @@ -0,0 +1,138 @@ +--- +name: Server-Side Rendering +menu: Usage +route: '/usage/ssr' +--- + +# Server-Side Rendering (SSR) + +## Setup + +To test how your hook will behave when rendered on the server, you can change your import to the use +the `server` module: + +```ts +import { renderHook } from '@testing-library/react-hooks/server' +``` + +> SSR is only available when using the `react-dom` renderer. Please refer to the +> [installation guide](/installation#peer-dependencies) for instructions and supported versions. + +This import has the same [API as the standard import](/reference/api) except the behaviour changes +to use SSR semantics. + +## Example + +## Hydration + +The result of rendering you hook is static are not interactive until it is hydrated into the DOM. +This can be done using the `hydrate` function that is returned from `renderHook`. + +Consider the `useCounter` example from the [Basic Hooks section](/usage/basic-hooks): + +```js +import { useState, useCallback } from 'react' + +export default function useCounter() { + const [count, setCount] = useState(0) + const increment = useCallback(() => setCount((x) => x + 1), []) + return { count, increment } +} +``` + +If we try to call `increment` immediately after server rendering, nothing happens and the hook is +not interactive: + +```js +import { renderHook, act } from '@testing-library/react-hooks/server' +import useCounter from './useCounter' + +test('should increment counter', () => { + const { result } = renderHook(() => useCounter(0)) + + act(() => { + result.current.increment() + }) + + expect(result.current.count).toBe(1) // fails as result.current.count is still 0 +}) +``` + +We can make the hook interactive by calling the `hydrate` function that is returned from +`renderHook`: + +```js +import { renderHook, act } from '@testing-library/react-hooks/server' +import useCounter from './useCounter' + +test('should increment counter', () => { + const { result, hydrate } = renderHook(() => useCounter(0)) + + hydrate() + + act(() => { + result.current.increment() + }) + + expect(result.current.count).toBe(1) // now it passes +}) +``` + +Anything that causes the hook's state to change will not work until `hydrate` is called. This +includes both the [`rerender`](http://localhost:3000/reference/api#rerender) and +[`unmount`](http://localhost:3000/reference/api#unmount) functionality. + +### Effects + +Another caveat of SSR is that `useEffect` and `useLayoutEffect` hooks, by design, do not run on when +rendering. + +Consider this `useTimer` hook: + +```js +import { useState, useCallback, useEffect } from 'react' + +export default function useTimer() { + const [count, setCount] = useState(0) + const reset = useCallback(() => setCount(0), []) + useEffect(() => { + const intervalId = setInterval(() => setCount((c) => c + 1, 1000)) + return () => { + clearInterval(intervalId) + } + }) + return { count, reset } +} +``` + +Upon initial render, the interval will not start: + +```js +import { renderHook, act } from '@testing-library/react-hooks/server' +import useTimer from './useTimer' + +test('should start the timer', async () => { + const { result, waitForValueToChange } = renderHook(() => useTimer(0)) + + await waitForValueToChange(() => result.current.count) // times out as the value never changes + + expect(result.current.count).toBe(1) // fails as result.current.count is still 0 +}) +``` + +Similarly to updating the hooks state, the effect will start after `hydrate` is called: + +```js +import { renderHook, act } from '@testing-library/react-hooks/server' +import useTimer from './useTimer' + +test('should start the timer', async () => { + const { result, hydrate, waitForValueToChange } = renderHook(() => useTimer(0)) + + hydrate() + + await waitForValueToChange(() => result.current.count) // now resolves when the interval fires + + expect(result.current.count).toBe(1) +}) +``` diff --git a/doczrc.js b/doczrc.js index 965cfbe2..3027121d 100644 --- a/doczrc.js +++ b/doczrc.js @@ -18,7 +18,8 @@ export default { }, menu: [ { name: 'Introduction' }, - { name: 'Usage', menu: ['Basic Hooks', 'Advanced Hooks'] }, + { name: 'Installation' }, + { name: 'Usage', menu: ['Basic Hooks', 'Advanced Hooks', 'Server-Side Rendering'] }, { name: 'API Reference' } ] }