Mode
Color
Width

Unit Testing and React Hook Form

October 3, 2023
3 Minute Read

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.