React Hooks Testing Library
Edit page
IntroductionInstallation
Usage
Basic HooksAdvanced HooksServer-Side RenderingSetupHydration
API Reference

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

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