skip to Main Content

I am using React Hook Form to handle form validation in my React project. While everything works fine when using plain elements, I encounter issues when I try to wrap the inputs in a custom component. Specifically, validations such as minLength and maxLength are not being triggered properly. It is always countering the required validation.

Here’s an example of my setup:
Parent Component (Parent.jsx):

import { useForm } from "react-hook-form";
import Input from "./Components/Input.jsx";
import Button from "./Components/Button.jsx";

export default function Parent() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  const login = (data) => {
    console.log("Form Data:", data); 
  };

  return (
    <div className="App">
      <div className="container">
        <form onSubmit={handleSubmit(login)}>
          <Input
            type="text"
            name="username"
            {...register("username", {
              required: {
                value: true,
                message: "Username is required",
              },
              minLength: {
                value: 5,
                message: "Too Few Characters",
              },
               maxLength: {
                value: 15,
                message: "username length should not exceed 15",
              },
            })}
          />
          {errors.username && <p className="red">{errors.username.message}</p>}

          <Input
            type="password"
            name="password"
            {...register("password", {
              required: {
                value: true,
                message: "password is required",
              },
              minLength: {
                value: 6,
                message: "Password length should be greater than 6",
              },
              maxLength: {
                value: 15,
                message: "Password length should not exceed 15",
              },
            })}
          />
          {errors.password && <p className="error-red">{errors.password.message}</p>}

          <Button type="submit" />
        </form>
      </div>
    </div>
  );
}

custom Input Component (Input.jsx):

import { forwardRef } from "react";
const Input = forwardRef(function Input(
  { type = "text", name = "", ...props },
  ref
) {
  return (
   <>
      <input
        placeholder=" "
        className="txt-input"
        type={type}
        ref={ref}
        {...props}
      />
      {name && (
        <label className="label-placeholder">
          {name.charAt(0).toUpperCase() + name.slice(1)}
        </label>
      )}
   </>
  );
});

export default Input;

  1. Using forwardRef to forward the ref from React Hook Form to the native .
  2. Passing all props (e.g., onChange, onBlur) from the parent component to the custom component.

2

Answers


  1. Looking at the Docs for Advanced Usage, it looks like you need to call register directly on the low-level components (e.g <input />)

    When we are building forms, there are times when our input lives inside of deeply nested component trees, and that’s when FormContext comes in handy

    The example code they use (under the "Connect Form" heading):

    import { FormProvider, useForm, useFormContext } from "react-hook-form"
    
    export const ConnectForm = ({ children }) => {
      const methods = useFormContext()
    
      return children({ ...methods })
    }
    
    export const DeepNest = () => (
      <ConnectForm>
        {({ register }) => <input {...register("deepNestedInput")} />}
      </ConnectForm>
    )
    
    export const App = () => {
      const methods = useForm()
    
      return (
        <FormProvider {...methods}>
          <form>
            <DeepNest />
          </form>
        </FormProvider>
      )
    }
    

    The key part of this is making sure that you’re using register from the Context that is created (likely from FormProvider and made accessible from useFormContext); it’s likely this is an abstracted version of useContext and createContext

    In your example it likely means the code would look like this:

    import { FormProvider, useForm, useFormContext } from "react-hook-form"
    
    export const ConnectForm = ({ children }) => {
      const methods = useFormContext()
    
      return children({ ...methods })
    }
    
    export const CustomInput = ({ name, registerProps }) => (
      <ConnectForm>
        {({ register }) => <input name={name} {...register(name, registerProps)} />}
      </ConnectForm>
    )
    
    export const App = () => {
      const methods = useForm()
    
      return (
        <FormProvider {...methods}>
          <form>
            <CustomInput
              name={"username"}
              registerProps={{
                required: {
                  value: true,
                  message: "Username is required",
                },
                minLength: {
                  value: 5,
                  message: "Too Few Characters",
                },
                 maxLength: {
                  value: 15,
                  message: "username length should not exceed 15",
                },
              }}
            />
          ...
          </form>
        </FormProvider>
      )
    }
    

    Note: You may not need to pass name={name} in the CustomInput, as I would expect register to create that prop already

    Login or Signup to reply.
  2. The Input component should pass/forward all field props to the underlying input element, e.g. the missing name prop so react-hook-form can validate the field data correctly.

    And for accessibility, you may also want to connect the rendered label to the input since it’s not wrapping it so when it’s clicked/interacted with the input is focused.

    Example:

    const Input = forwardRef(function Input(
      { name = "", type = "text", ...props },
      ref
    ) {
      return (
        <>
          <input
            ref={ref}
            className="txt-input"
            id={name}   // <-- for accessibility with label
            name={name} // <-- for field validation
            placeholder=" "
            type={type}
            {...props}
          />
          {name && (
            <label
              className="label-placeholder"
              htmlFor={name} // <-- for accessibility with input
            >
              {name.charAt(0).toUpperCase() + name.slice(1)}
            </label>
          )}
        </>
      );
    });
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search