Testing React Hooks

https://commons.wikimedia.org/wiki/File:React-icon.svg

If you’ve been following react community recently, you might notice the hype around the latest proposal about Hooks.

I think they are awesome😎

Dan Abramov wrote an article to give us a first glance and the problems that Hooks are trying to solve. Within a short period of time since the proposal was announced, there are so many creative custom Hooks already that you can experiment with. If you still aren’t quite sure about them, check out Ryan Florence’s amazing talk “90% Cleaner React with Hooks” at React Conf 2018.

Ryan Florence at React Conf 2018

With Hooks, we now have a pattern to tap into state and React features like context with functional components. Custom Hooks allow us to separate cross-cutting concerns from components so we can more intuitively reuse logic without Render Props or Higher Order Components. Since custom Hooks are standalone units, we want to test them independently.

Let’s get to it!

https://gph.is/SkI3MM

Let’s say we are writing a custom Hook to set viewport size that aligns with Bootstrap’s grid system on window resize.

// useViewport.js
import { useState, useEffect } from 'react'

export default function useViewport() {
const [viewport, setViewport] = useState()

const handleResize = () => {
if (window.innerWidth > 1200) {
setViewport('extra-large')
} else if (window.innerWidth > 992) {
setViewport('large')
} else if (window.innerWidth > 768) {
setViewport('medium')
} else if (window.innerWidth > 576) {
setViewport('small')
} else {
setViewport('extra-small')
}
};

useEffect(() => {
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
})

return viewport;
}

We created a viewport variable in the state with useState and update it with useEffect when window resizes.

We can’t simply just test the useViewport function because Hooks are part components. Even though we abstract it out as a standalone function, we still rely on components’ rendering to update their states. So we’ll have to create a component to use useViewport and simulate window resize in our test.

// useViewport.test.js
import React from 'react'
import { render } from 'react-testing-library'

import useViewport from './useViewport'
// simulate window resize 
function fireResize(width) {
window.innerWidth = width
window.dispatchEvent(new Event('resize'))
}
// Test component that uses the Hook
function EffecfulComponent() {
const viewport = useViewport()
return <span>{viewport}</span>
}

test('useViewport listen to window resize and set viewport size responsively', () => {
const { container, rerender } = render(<EffecfulComponent />)
const span = container.firstChild

fireResize(320)

// useEffect is triggered after rendering.
// So we want to rerender the component to see the state change
rerender(<EffecfulComponent />)
expect(span.textContent).toBe('extra-small')
    fireResize(600)

rerender(<EffecfulComponent />)
expect(span.textContent).toBe('small')
    fireResize(800)

rerender(<EffecfulComponent />)
expect(span.textContent).toBe('medium')

fireResize(1000)

rerender(<EffecfulComponent />)
expect(span.textContent).toBe('large')

fireResize(1280)

rerender(<EffecfulComponent />)
expect(span.textContent).toBe('extra-large')
})

All we did was

1. Create a test component.
2. Trigger resize event
3. Re-render test component
4. Assert our viewport value

We re-render the test component every time after each resize event because useEffect happens after every render to handle side effects.

Here you have it! You can also find the code snippets in this gist.


Let me know what you think about testing Hooks and definitely drop me a comment on better ways to test them!

If you’re a fan of functional programming, check out this article that I wrote about Transducers. It’s a step by step reasoning on writing a transducer and it touches on key ideas about functional programming.

Drop me a comment or find me on twitter to share your thoughts about React Hooks!