You don’t write components from scratch every time.
Maintaining existing parts of the system that have been working without an issue for years is also part of your job.
And as the programming mantra says:
If it ain’t broke, don’t fix it!
Not touching something that works is an excellent rule of thumb. But what if we want to do some good and add some tests to these components to ensure they’ll keep doing their thing for years?
I found the most use for React Testing Library’s rerender function when working with legacy codebase, both tests and components.
The documentation starts with a disclaimer, essentially saying you probably want to test the component that is making the prop updates.
It’d probably be better if you test the component that’s doing the prop updating to ensure that the props are being updated correctly (see the Guiding Principles section).
However, that can be either complicated, requiring too much setup, or something else preventing you from testing the component causing the prop updates.
So let’s look at today’s example, a simple message board:
export default function MessageBoard({ submitMessage, error }: Props) {
const [message, setMessage] = useState("");
function handleSubmit(e: FormEvent) {
e.preventDefault();
submitMessage(message);
}
return (
<form onSubmit={handleSubmit}>
<label>
Message:
<input onChange={(e) => setMessage(e.target.value)} value={message} />
</label>
<input type="submit" value="Submit" />
{error ? <p>{error}</p> : null}
</form>
);
}
The MessageBoard component does two things:
- saves the value of the Message field
- displays the error message coming in as a prop
We want to test that the component displays different error messages while leaving the Message input unchanged.
Let’s see how we can achieve this!
👯♂️ Render twice?
Why can’t we do this simply with render
?
Well, once you render the component with a specific error message and change the input, there isn’t a way to specify a different error. You might try this:
render(
<ScoreBoard submitScore={submitScoreMock} error={"Initial error"} />,
);
await userEvent.type(
screen.getByLabelText("Message:"),
"Hello from test!"
);
render(
<ScoreBoard submitScore={submitScoreMock} error={"Different error"} />,
);
await userEvent.type(
screen.getByLabelText("Message:"),
"Hello again!"
);
But if you render
twice inside the same test, all you get is an entirely new component inside the DOM:
This will lead to errors when trying to interact with the component because essentially everything would be doubled in the DOM, and queries would fail:
TestingLibraryElementError: Found multiple elements with the text of: Message:
Here are the matching elements:
Ignored nodes: comments, script, style
<input
value="Hello from test!"
/>
Ignored nodes: comments, script, style
<input
value=""
/>
(If this is intentional, then use the `*AllBy*` variant of the query (like `queryAllByText`, `getAllByText`, or `findAllByText`)).
♻️ Turn render into rerender
Luckily, render returns an object that, besides other things, contains a rerender function.
const { rerender } = render(
<MessageBoard submitMessage={submitMessageMock} error={"Add a score"} />,
);
rerender
is a function that can supply new props to your component, but instead of creating a new component in the DOM, like render, it’ll re-render the existing component with the new props.
You can simply verify this by putting some screen.debug()
before the rerender
and after it:
So, while the input contents changed and we added a new text, we still had a single component in the DOM, unlike when we used render
twice.
For reference, here’s the entire test without the screen.debug()
:
🎯 Conclusion
I hope this short post sheds some light on which use cases could benefit from the usage of rerender.
Remember, the best is to test the component that causes the prop updates, but in some cases, you might want to stick to rerender.
Thanks for reading!