Why you shouldn't put refs in a dependency array
If you use a ref in your effect callback, shouldn't it be included in the dependencies? Why refs are a special exception to the rule!
So, you've got some code in React.useEffect
and you want to know how to test it. This is a pretty common question. The answer is kinda anti-climatic and general. Here's how you think about testing anything:
How does the user make that code run? Make your test do that.
That's it. That's the secret. The trick is discovering what constitutes a "user." Your React components actually have 2 users: the developer who renders it and the end-user who interacts with it. Generally, your tests should do no more or less than what these users do. For more on this, read Avoid the Test User.
So, let's take a look at an example. I'll grab some code from one of the exercises in EpicReact.dev/app (it's a pretty long example, feel free to scan it quickly):
/** @jsx jsx */import {jsx} from '@emotion/core'import * as React from 'react'import Tooltip from '@reach/tooltip'import {FaSearch, FaTimes} from 'react-icons/fa'import {Input, BookListUL, Spinner} from './components/lib'import {BookRow} from './components/book-row'import {client} from './utils/api-client'import * as colors from './styles/colors'function DiscoverBooksScreen() { const [status, setStatus] = React.useState('idle') const [data, setData] = React.useState() const [error, setError] = React.useState() const [query, setQuery] = React.useState() const [queried, setQueried] = React.useState(false) const isLoading = status === 'loading' const isSuccess = status === 'success' const isError = status === 'error' React.useEffect(() => { if (!queried) { return } setStatus('loading') client(`books?query=${encodeURIComponent(query)}`).then( (responseData) => { setData(responseData) setStatus('success') }, (errorData) => { setError(errorData) setStatus('error') }, ) }, [query, queried]) function handleSearchSubmit(event) { event.preventDefault() setQueried(true) setQuery(event.target.elements.search.value) } return ( <div css={{maxWidth: 800, margin: 'auto', width: '90vw', padding: '40px 0'}} > <form onSubmit={handleSearchSubmit}> <Input placeholder="Search books..." id="search" css={{width: '100%'}} /> <Tooltip label="Search Books"> <label htmlFor="search"> <button type="submit" css={{ border: '0', position: 'relative', marginLeft: '-35px', background: 'transparent', }} > {isLoading ? ( <Spinner /> ) : isError ? ( <FaTimes aria-label="error" css={{color: colors.danger}} /> ) : ( <FaSearch aria-label="search" /> )} </button> </label> </Tooltip> </form> {isError ? ( <div css={{color: colors.danger}}> <p>There was an error:</p> <pre>{error.message}</pre> </div> ) : null} {isSuccess ? ( data?.books?.length ? ( <BookListUL css={{marginTop: 20}}> {data.books.map((book) => ( <li key={book.id} aria-label={book.title}> <BookRow key={book.id} book={book} /> </li> ))} </BookListUL> ) : ( <p>No books found. Try another search.</p> ) ) : null} </div> )}export {DiscoverBooksScreen}
Keep in mind that the above example would be better with
useReducer
and we > get to improving this later in the workshop exercises.
Let's look at this bit specifically:
React.useEffect(() => { if (!queried) { return } setStatus('loading') client(`books?query=${encodeURIComponent(query)}`).then( (responseData) => { setData(responseData) setStatus('success') }, (errorData) => { setError(errorData) setStatus('error') }, )}, [query, queried])function handleSearchSubmit(event) { event.preventDefault() setQueried(true) setQuery(event.target.elements.search.value)}
Because we've properly mocked our backend using MSW (learn more about that in Stop Mocking Fetch), we can actually make that request and get results. So let's interact with this component just the same way the end user would. Here's a test that actually works with this code:
import * as React from 'react'import { render, screen, waitForElementToBeRemoved, within,} from '@testing-library/react'import userEvent from '@testing-library/user-event'import {DiscoverBooksScreen} from '../discover.extra-1'// Learn more: https://kentcdodds.com/blog/stop-mocking-fetchimport {server} from 'test/server'beforeAll(() => server.listen())afterAll(() => server.close())afterEach(() => server.resetHandlers())test('queries for books', async () => { // 🤓 this is what developer users do render(<DiscoverBooksScreen />) // 🤠 this is what end users do userEvent.type( screen.getByRole('textbox', {name: /search/i}), 'Sanderson{enter}', ) // 🤠 end users will also note the presence of the loading indicator // and wait until it's gone before making som assertions await waitForElementToBeRemoved(() => screen.getByLabelText(/loading/i)) // 🤠 end users will look at all the items in the list to see the book titles // Also, assistive technologies will take advantage of the implicit "listitem" // ARIA role of our `li` elements. const results = screen.getAllByRole('listitem').map((listItem) => { return within(listItem).getByRole('heading', {level: 2}).textContent }) // I rarely use snapshots, but this seemed like a pretty good application: // https://kcd.im/snapshots expect(results).toMatchInlineSnapshot(` Array [ "The Way of Kings (Book 1 of the Stormlight Archive)", "Words of Radiance (Book 2 of the Stormlight Archive)", "Oathbringer (Book 3 of the Stormlight Archive)", ] `)})
There are other ways I could write this test (there are some important things that happen in that test/server
module that we don't have time to cover in this post), but the principles are all the same:
How does the user make that code run? Make your test do that.
Stated differently:
The more your tests resemble the way your software is used, the more > confidence they can give you. > – me
So don't try mocking useEffect
or useState
or whatever. Stay away from that third user (the dreaded test user). The only thing that user is good for is turning you into a glorified test babysitter. And I don't know about you, but I'd rather ship awesome stuff to real people.
Delivered straight to your inbox.
If you use a ref in your effect callback, shouldn't it be included in the dependencies? Why refs are a special exception to the rule!
Forms can get slow pretty fast. Let's explore how state colocation can keep our React forms fast.
The sneaky, surreptitious bug that React saved us from by using closures.
When was the last time you saw an error and had no idea what it meant (and therefore no idea what to do about it)? Today? Yeah, you're not alone... Let's talk about how to fix that.