I'm using react-hook-form to validate my input but I can't get it to work after extracting the input to its own component

Viewed 5314

I have a Form.js component which returns a form element. This form element contains a FormGroup component which takes props such as inputType, inputName, inputPlaceholder and in return renders an input field with a label. I am using react-hook-form for the validation of the input but I can't get it to work after extracting the input to a separate component. In my original code, I had my errors appear when the validation failed, but after extracting the label and input into their own component, this stopped working.

My original working code looks like this:

import { useState } from 'react';
import { useForm } from 'react-hook-form';
import FormGroup from './FormGroup';

const Form = () => {
    const { register, handleSubmit, formState: { errors } } = useForm();

    const onSubmit = async (data) => {
        console.log(data)
    };

    return (
        <form className="form-container" onSubmit={handleSubmit(onSubmit)} autoComplete="off" noValidate>
            <div className="form-group">
                <label className="form-label" htmlFor="firstName">
                    <p>First Name</p>
                    <p className="input-error">{errors.firstName && errors.firstName.message}</p>
                </label>
                <input type="text" name="firstName" placeholder="First Name" {...register("firstName", { required: 'Required ' })} />
            </div>

            <div className="form-group">
                <button type="submit">Submit</button>
            </div>
        </form>
    );
}

export default Form

Then I changed it to:

import { useState } from 'react';
import { useForm } from 'react-hook-form';
import FormGroup from './FormGroup';

const Form = () => {
    const { register, handleSubmit, formState: { errors } } = useForm();

    const onSubmit = async (data) => {
        console.log(data)
    };

    return (
        <form className="form-container" onSubmit={handleSubmit(onSubmit)} autoComplete="off" noValidate>
            <FormGroup
                inputType="text"
                inputName="firstName"
                inputPlaceholder="First Name">
            </FormGroup>

            <div className="form-group">
                <button type="submit">Submit</button>
            </div>
        </form>
    );
}

export default Form

And I extracted the label and input into:

import { useForm } from 'react-hook-form';

const FormGroup = (props) => {
    const { register, formState: { errors } } = useForm();

    return (
        <div className="form-group">
            <label className="form-label" htmlFor={props.inputName}>
                <p>{ props.inputPlaceholder }</p>
                <p className="input-error">{errors.firstName && errors.firstName.message}</p>
            </label>
            <input type={props.inputType} name={props.inputName} placeholder={props.inputPlaceholder} {...register(props.inputName, { required: true })} />
        </div>
    );
}

export default FormGroup
2 Answers

The Problem

You have multiple instances of the useForm hook.

Solution

Use the useForm hook only on the Form component and pass the errors object and register method as props to FormGroup

Form Component

import { useState } from 'react';
import { useForm } from 'react-hook-form';
import FormGroup from './FormGroup';

const Form = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  const onSubmit = async (data) => {
    console.log(data);
  };

  return (
    <form
      className="form-container"
      onSubmit={handleSubmit(onSubmit)}
      autoComplete="off"
      noValidate
    >
      <FormGroup
        inputType="text"
        inputName="firstName"
        inputPlaceholder="First Name"
        register={register}
        errors={errors}
      ></FormGroup>

      <div className="form-group">
        <button type="submit">Submit</button>
      </div>
    </form>
  );
};

export default Form;

FormGroup Component

import { useForm } from 'react-hook-form';

const FormGroup = (props) => {
  return (
    <div className="form-group">
      <label className="form-label" htmlFor={props.inputName}>
        <p>{props.inputPlaceholder}</p>
        <p className="input-error">
          {props.errors.firstName && props.errors.firstName.message}
        </p>
      </label>
      <input
        type={props.inputType}
        name={props.inputName}
        placeholder={props.inputPlaceholder}
        {...props.register(props.inputName, { required: true })}
      />
    </div>
  );
};

export default FormGroup;

The recommended approach from the Documentation is as follows:

useFormContext This custom hook allows you to access the form context. useFormContext is intended to be used in deeply nested structures, where it would become inconvenient to pass the context as a prop. https://react-hook-form.com/api/useformcontext

const Form = () => {
  const methods = useForm();
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = methods;

  const onSubmit = async (data) => {
    console.log(data);
  };

  return (
    <FormProvider {...methods} > 
      <form
        className="form-container"
        onSubmit={handleSubmit(onSubmit)}
        autoComplete="off"
        noValidate
      >
        <FormGroup
          inputType="text"
          inputName="firstName"
          inputPlaceholder="First Name">
        </FormGroup>

        <div className="form-group">
          <button type="submit">Submit</button>
        </div>
      </form>
    </FormProvider>
  );
};
const FormGroup = (props) => {
  const { register, errors } = useFormContext(); // retrieve all hook methods from parent
  return (
    <div className="form-group">
      <label className="form-label" htmlFor={props.inputName}>
        <p>{props.inputPlaceholder}</p>
        <p className="input-error">
          {errors.firstName && errors.firstName.message}
        </p>
      </label>
      <input
        type={props.inputType}
        name={props.inputName}
        placeholder={props.inputPlaceholder}
        {register(props.inputName, { required: true })}
      />
    </div>
  );
};

export default FormGroup;
Related