|
1 | 1 | ---
|
2 |
| -name: SSR Hooks |
| 2 | +--- |
| 3 | +name: Server-Side Rendering |
3 | 4 | menu: Usage
|
4 | 5 | route: '/usage/ssr-hooks'
|
5 | 6 | ---
|
6 | 7 |
|
7 |
| -# Server Side Renderering |
| 8 | +# Server-Side Rendering (SSR) |
8 | 9 |
|
9 | 10 | ## Setup
|
10 | 11 |
|
11 |
| -To SSR your hook, you must ensure `react-dom >= 16.9.0` is installed in your project and then import |
12 |
| -the server module in your test: |
| 12 | +To test how your hook will behave when rendered on the server, you can change your import to the use |
| 13 | +the `server` module: |
13 | 14 |
|
14 | 15 | ```ts
|
15 | 16 | import { renderHook } from '@testing-library/react-hooks/server'
|
16 | 17 | ```
|
17 | 18 |
|
18 |
| -## Render Hook |
| 19 | +> SSR is only available when using the `react-dom` renderer. Please refer to the |
| 20 | +> [installation guide](/installation#peer-dependencies) for instructions and supported versions. |
19 | 21 |
|
20 |
| -`renderHook` when called returns the same result as documented in the |
21 |
| -[API](/reference/api#renderhook-result) but includes an additional argument, `hydrate`: |
| 22 | +This import has the same [API as the standard import](/reference/api) except the behaviour changes |
| 23 | +to use SSR semantics. |
22 | 24 |
|
23 |
| -```ts |
24 |
| -function hydrate(): void |
25 |
| -``` |
| 25 | +## Example |
26 | 26 |
|
27 |
| -The `hydrate` function is a light wrapper around |
28 |
| -[`ReactDOM.hydrate`](https://reactjs.org/docs/react-dom.html#hydrate) but no arguments are required |
29 |
| -as the library will pass the element & container for you. Remember, certain effects such as |
30 |
| -`useEffect` will not run server side and `hydrate` must be called before those effects are |
31 |
| -ran.`hydrate`is also necessary before the first `act` or `rerender` call. For more information on |
32 |
| -`hydrate` see the [API documentation](/reference/api#hydrate). There is also an |
33 |
| -[example below](/usage/ssr-hooks#example) |
| 27 | +## Hydration |
34 | 28 |
|
35 |
| -## Example |
| 29 | +The result of rendering you hook is static are not interactive until it is hydrated into the DOM. |
| 30 | +This can be done using the `hydrate` function that is returned from `renderHook`. |
| 31 | + |
| 32 | +Consider the `useCounter` example from the [Basic Hooks section](/usage/basic-hooks): |
| 33 | + |
| 34 | +```js |
| 35 | +import { useState, useCallback } from 'react' |
| 36 | + |
| 37 | +export default function useCounter() { |
| 38 | + const [count, setCount] = useState(0) |
| 39 | + const increment = useCallback(() => setCount((x) => x + 1), []) |
| 40 | + return { count, increment } |
| 41 | +} |
| 42 | +``` |
36 | 43 |
|
37 |
| -### Hydration |
| 44 | +If we try to call `increment` immediately after server rendering, nothing happens and the hook is |
| 45 | +not interactive: |
38 | 46 |
|
39 | 47 | ```js
|
40 | 48 | import { renderHook, act } from '@testing-library/react-hooks/server'
|
| 49 | +import useCounter from './useCounter' |
41 | 50 |
|
42 |
| -describe('custom hook tests', () => { |
43 |
| - function useCounter() { |
44 |
| - const [count, setCount] = useState(0) |
| 51 | +test('should increment counter', () => { |
| 52 | + const { result } = renderHook(() => useCounter(0)) |
45 | 53 |
|
46 |
| - const increment = useCallback(() => setCount(count + 1), [count]) |
47 |
| - const decrement = useCallback(() => setCount(count - 1), [count]) |
| 54 | + act(() => { |
| 55 | + result.current.increment() |
| 56 | + }) |
48 | 57 |
|
49 |
| - return { count, increment, decrement } |
50 |
| - } |
| 58 | + expect(result.current.count).toBe(1) // fails as result.current.count is still 0 |
| 59 | +}) |
| 60 | +``` |
51 | 61 |
|
52 |
| - test('should decrement counter', () => { |
53 |
| - const { result, hydrate } = renderHook(() => useCounter()) |
| 62 | +We can make the hook interactive by calling the `hydrate` function that is returned from |
| 63 | +`renderHook`: |
54 | 64 |
|
55 |
| - expect(result.current.count).toBe(0) |
| 65 | +```js |
| 66 | +import { renderHook, act } from '@testing-library/react-hooks/server' |
| 67 | +import useCounter from './useCounter' |
56 | 68 |
|
57 |
| - // hydrate is called because we want to interact with the hook |
58 |
| - hydrate() |
| 69 | +test('should increment counter', () => { |
| 70 | + const { result, hydrate } = renderHook(() => useCounter(0)) |
59 | 71 |
|
60 |
| - act(() => result.current.decrement()) |
| 72 | + hydrate() |
61 | 73 |
|
62 |
| - expect(result.current.count).toBe(-1) |
| 74 | + act(() => { |
| 75 | + result.current.increment() |
63 | 76 | })
|
| 77 | + |
| 78 | + expect(result.current.count).toBe(1) // now it passes |
64 | 79 | })
|
65 | 80 | ```
|
66 | 81 |
|
| 82 | +Anything that causes the hook's state to change will not work until `hydrate` is called. This |
| 83 | +includes both the [`rerender`](http://localhost:3000/reference/api#rerender) and |
| 84 | +[`unmount`](http://localhost:3000/reference/api#unmount) functionality. |
| 85 | + |
67 | 86 | ### Effects
|
68 | 87 |
|
| 88 | +Another caveat of SSR is that `useEffect` and `useLayoutEffect` hooks, by design, do not run on when |
| 89 | +rendering. |
| 90 | + |
| 91 | +Consider this `useTimer` hook: |
| 92 | + |
69 | 93 | ```js
|
70 |
| -describe('useEffect tests', () => { |
71 |
| - test('should handle useEffect hook', () => { |
72 |
| - const sideEffect = { 1: false, 2: false } |
73 |
| -
|
74 |
| - const useEffectHook = ({ id }) => { |
75 |
| - useEffect(() => { |
76 |
| - sideEffect[id] = true |
77 |
| - return () => { |
78 |
| - sideEffect[id] = false |
79 |
| - } |
80 |
| - }, [id]) |
| 94 | +import { useState, useCallback, useEffect } from 'react' |
| 95 | + |
| 96 | +export default function useTimer() { |
| 97 | + const [count, setCount] = useState(0) |
| 98 | + const reset = useCallback(() => setCount(0), []) |
| 99 | + useEffect(() => { |
| 100 | + const intervalId = setInterval(() => setCount((c) => c + 1, 1000)) |
| 101 | + return () => { |
| 102 | + clearInterval(intervalId) |
81 | 103 | }
|
| 104 | + }) |
| 105 | + return { count, reset } |
| 106 | +} |
| 107 | +``` |
82 | 108 |
|
83 |
| - const { hydrate, rerender, unmount } = renderHook((id) => useEffectHook({ id }), { |
84 |
| - initialProps: { id: 1 } |
85 |
| - }) |
| 109 | +Upon initial render, the interval will not start: |
86 | 110 |
|
87 |
| - expect(sideEffect[1]).toBe(false) |
88 |
| - expect(sideEffect[2]).toBe(false) |
| 111 | +```js |
| 112 | +import { renderHook, act } from '@testing-library/react-hooks/server' |
| 113 | +import useTimer from './useTimer' |
89 | 114 |
|
90 |
| - hydrate() |
| 115 | +test('should start the timer', async () => { |
| 116 | + const { result, waitForValueToChange } = renderHook(() => useTimer(0)) |
91 | 117 |
|
92 |
| - expect(sideEffect[1]).toBe(true) |
93 |
| - expect(sideEffect[2]).toBe(false) |
94 |
| - }) |
| 118 | + await waitForValueToChange(() => result.current.count) // times out as the value never changes |
| 119 | + |
| 120 | + expect(result.current.count).toBe(1) // fails as result.current.count is still 0 |
| 121 | +}) |
| 122 | +``` |
| 123 | + |
| 124 | +Similarly to updating the hooks state, the effect will start after `hydrate` is called: |
| 125 | + |
| 126 | +```js |
| 127 | +import { renderHook, act } from '@testing-library/react-hooks/server' |
| 128 | +import useTimer from './useTimer' |
| 129 | + |
| 130 | +test('should start the timer', async () => { |
| 131 | + const { result, hydrate, waitForValueToChange } = renderHook(() => useTimer(0)) |
| 132 | + |
| 133 | + hydrate() |
| 134 | + |
| 135 | + await waitForValueToChange(() => result.current.count) // now resolves when the interval fires |
| 136 | + |
| 137 | + expect(result.current.count).toBe(1) |
95 | 138 | })
|
96 | 139 | ```
|
0 commit comments