One of my side projects uses React Hook Form for my forms and ran into some speed bumps while building tests for the individual components. I figure I can’t be the only one so I figure I’ll share my solution.
The issue I was dealing with was that I was trying to use a react hook outside a react component.
test('It renders the control', () => {
const { register } = useForm();
render(
<TextInput
type="text"
id="name"
label="Name"
error=""
required
register={register}
/>
);
expect(screen.getByLabelText(/Name/)).toBeInTheDocument();
});
Yeah, that didn't work too well. Wasn't something I immediately thought of when I wrote the test. Vitest immediately yelled at me for it.
The big thing here is that I'm not testing a form as a whole, I'm testing an individual control I'm planning on using throughout the project. If I was testing a whole form, this wouldn't pose much of an issue as I wouldn't need to mock the hook.
After some searching, I realized that I had to put it in a wrapper. I created a helper function file in my tests directory.
import { useForm, FormProvider } from 'react-hook-form';
import { render } from '@testing-library/react';
import { ReactNode, ReactElement } from 'react';
const renderWithReactHookForm = (ui: ReactElement, { defaultValues = {} } = {}): any => {
const Wrapper = ({ children }: { children: ReactNode }) => {
const methods = useForm({ defaultValues });
return (
<FormProvider {...methods}>{children}</FormProvider>
);
}
return {
...render(ui, { wrapper: Wrapper }),
};
}
export default renderWithReactHookForm;
What this does is it creates a functional component wrapper for what I want to render, in this case my TextInput
control. The FormProvider allowed me to refactor my TextInput
control to not have to pass the form helper as a prop.
Because I'm using the FormProvider
now in both the wrapper and the form, I can grab the form context from the nested input control by using the following line:
const { register } = useFormContext<any>();
So now all I have to do is tweak the unit test slightly.
test('It renders the control', () => {
renderWithReactHookForm(
<TextInput
type="text"
id="name"
label="Name"
error=""
required
/>
);
expect(screen.getByLabelText(/Name/)).toBeInTheDocument();
});
I render the TextInput
control within the functional component so the react hook works, the control renders, and I can continue my tests.
I run into stuff like this when I'm working, especially with new libraries. I figured I'd share what I found through my searching and experimentation in hopes that it helps you or someone else.