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:
import { renderHook } from '@testing-library/react-hooks/server'
SSR is only available when using the
react-dom
renderer. Please refer to the installation guide for instructions and supported versions.
This import has the same API as the standard import except the behaviour changes to use SSR semantics.
Hydration
The result of rendering your hook is static and 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:
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:
import { renderHook, act } from '@testing-library/react-hooks/server'import useCounter from './useCounter'test('should increment counter', () => {const { result } = renderHook(() => useCounter())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
:
import { renderHook, act } from '@testing-library/react-hooks/server'import useCounter from './useCounter'test('should increment counter', () => {const { result, hydrate } = renderHook(() => useCounter())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
and 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:
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:
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 changesexpect(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:
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 firesexpect(result.current.count).toBe(1)})