---
id: example-react-hooks
title: React Hooks
---

`react-testing-library` provides the
[`testHook`](/docs/react-testing-library/api#testhook) utility to test custom
hooks.

> **Note**
>
> This is the recommended way to test reusable custom react hooks. It is not
> however recommended to use the testHook utility to test single-use custom
> hooks. Typically those are better tested by testing the component that is
> using it.

## Using `result`

Testing the last returned value of a hook using the `result` ref

```jsx
function useCounter({ initialCount = 0, step = 1 } = {}) {
  const [count, setCount] = React.useState(initialCount)
  const increment = () => setCount(c => c + step)
  const decrement = () => setCount(c => c - step)
  return { count, increment, decrement }
}
```

```jsx
test('returns result ref with latest result from hook execution', () => {
  const { result } = testHook(useCounter)
  expect(result.current.count).toBe(0)
  act(() => {
    result.current.increment()
  })
  expect(result.current.count).toBe(1)
})
```

## State

Testing a hook that provides state

```jsx
import { useState } from 'react'

export function useCounter({ initialCount = 0, step = 1 } = {}) {
  const [count, setCount] = useState(initialCount)
  const increment = () => setCount(c => c + step)
  const decrement = () => setCount(c => c - step)
  return { count, increment, decrement }
}
```

```jsx
import { testHook, act, cleanup } from 'react-testing-library'
afterEach(cleanup)

describe('useCounter', () => {
  test('accepts default initial values', () => {
    let count
    testHook(() => ({ count } = useCounter()))

    expect(count).toBe(0)
  })

  test('accepts a default initial value for `count`', () => {
    let count
    testHook(() => ({ count } = useCounter({})))

    expect(count).toBe(0)
  })

  test('provides an `increment` function', () => {
    let count, increment
    testHook(() => ({ count, increment } = useCounter({ step: 2 })))

    expect(count).toBe(0)
    act(() => {
      increment()
    })
    expect(count).toBe(2)
  })

  test('provides an `decrement` function', () => {
    let count, decrement
    testHook(() => ({ count, decrement } = useCounter({ step: 2 })))

    expect(count).toBe(0)
    act(() => {
      decrement()
    })
    expect(count).toBe(-2)
  })

  test('accepts a default initial value for `step`', () => {
    let count, increment
    testHook(() => ({ count, increment } = useCounter({})))

    expect(count).toBe(0)
    act(() => {
      increment()
    })
    expect(count).toBe(1)
  })
})
```

## Unmount Side-Effects

Using the `unmount` function to check useEffect behavior when unmounting

```jsx
import { useState, useEffect } from 'react'

export function useDocumentTitle(title) {
  const [originalTitle, setOriginalTitle] = useState(document.title)
  useEffect(() => {
    setOriginalTitle(document.title)
    document.title = title
    return () => {
      document.title = originalTitle
    }
  }, [title])
}
```

```jsx
describe('useDocumentTitle', () => {
  test('sets a title', () => {
    document.title = 'original title'
    testHook(() => {
      useDocumentTitle('modified title')
    })

    expect(document.title).toBe('modified title')
  })

  test('returns to original title when component is unmounted', () => {
    document.title = 'original title'
    const { unmount } = testHook(() => {
      useDocumentTitle('modified title')
    })

    unmount()
    expect(document.title).toBe('original title')
  })
})
```

## Rerender Side-Effects

Using the `rerender` function to test calling useEffect multiple times

```jsx
import { useEffect } from 'react'

export function useCall(callback, deps) {
  useEffect(() => {
    callback()
  }, deps)
}
```

```jsx
describe('useCall', () => {
  test('calls once on render', () => {
    const spy = jest.fn()
    testHook(() => {
      useCall(spy, [])
    })
    expect(spy).toHaveBeenCalledTimes(1)
  })

  test('calls again if deps change', () => {
    let deps = [false]
    const spy = jest.fn()
    const { rerender } = testHook(() => {
      useCall(spy, deps)
    })
    expect(spy).toHaveBeenCalledTimes(1)

    deps = [true]
    rerender()
    expect(spy).toHaveBeenCalledTimes(2)
  })

  test('does not call again if deps are the same', () => {
    let deps = [false]
    const spy = jest.fn()
    const { rerender } = testHook(() => {
      useCall(spy, deps)
    })
    expect(spy).toHaveBeenCalledTimes(1)

    deps = [false]
    rerender()
    expect(spy).toHaveBeenCalledTimes(1)
  })
})
```