Skip to content

Commit 1aa5465

Browse files
joshuaellismpeyper
andauthored
docs: update with suggestions
Co-authored-by: Michael Peyper <[email protected]>
1 parent 05c9af5 commit 1aa5465

File tree

1 file changed

+96
-53
lines changed

1 file changed

+96
-53
lines changed

docs/usage/ssr-hooks.md

+96-53
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,139 @@
11
---
2-
name: SSR Hooks
2+
---
3+
name: Server-Side Rendering
34
menu: Usage
45
route: '/usage/ssr-hooks'
56
---
67

7-
# Server Side Renderering
8+
# Server-Side Rendering (SSR)
89

910
## Setup
1011

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:
1314

1415
```ts
1516
import { renderHook } from '@testing-library/react-hooks/server'
1617
```
1718

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.
1921
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.
2224

23-
```ts
24-
function hydrate(): void
25-
```
25+
## Example
2626

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
3428

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+
```
3643

37-
### Hydration
44+
If we try to call `increment` immediately after server rendering, nothing happens and the hook is
45+
not interactive:
3846

3947
```js
4048
import { renderHook, act } from '@testing-library/react-hooks/server'
49+
import useCounter from './useCounter'
4150

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))
4553

46-
const increment = useCallback(() => setCount(count + 1), [count])
47-
const decrement = useCallback(() => setCount(count - 1), [count])
54+
act(() => {
55+
result.current.increment()
56+
})
4857

49-
return { count, increment, decrement }
50-
}
58+
expect(result.current.count).toBe(1) // fails as result.current.count is still 0
59+
})
60+
```
5161

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`:
5464

55-
expect(result.current.count).toBe(0)
65+
```js
66+
import { renderHook, act } from '@testing-library/react-hooks/server'
67+
import useCounter from './useCounter'
5668

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))
5971

60-
act(() => result.current.decrement())
72+
hydrate()
6173

62-
expect(result.current.count).toBe(-1)
74+
act(() => {
75+
result.current.increment()
6376
})
77+
78+
expect(result.current.count).toBe(1) // now it passes
6479
})
6580
```
6681

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+
6786
### Effects
6887

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+
6993
```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)
81103
}
104+
})
105+
return { count, reset }
106+
}
107+
```
82108

83-
const { hydrate, rerender, unmount } = renderHook((id) => useEffectHook({ id }), {
84-
initialProps: { id: 1 }
85-
})
109+
Upon initial render, the interval will not start:
86110

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'
89114

90-
hydrate()
115+
test('should start the timer', async () => {
116+
const { result, waitForValueToChange } = renderHook(() => useTimer(0))
91117

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)
95138
})
96139
```

0 commit comments

Comments
 (0)