Using React Hook Form is a great way to handle inputs without managing state heavily.
As the official guide mentioned, you can easily pass the necessary props like this:
<input {...register("name")} />
However, when you try to make a custom input component that behaves like the native one but with extra functionalities like this:
// Takes a "label" prop along with all <input /> props
const LabeledInput = ({ label, ...props }) => (
<label>
{label}
<input {...props} />
</label>
);
...
<LabeledInput label="Name" placeholder="Enter your name..." required />
And try to register the input the same way:
<LabeledInput label="Name" {...register} />
Yes, you can still type in that input field, but when you try to submit the form, you can’t get the values that you’ve put in.
To solve this, we should first understand what {…register} actually does. If we look into the official docs on register, we are essentially passing these props:
const { onChange, onBlur, name, ref } = register('name');
...
<input onChange={onChange} onBlur={onBlur} name={name} ref={ref} />
While the first three seem simple enough to pass as props, the last one is a bit tricky. You can’t really pass a ref like that.
One way to fix it is to pass the ref in an additional prop, like:
const registerWithRef = (name) => {
const { ref, ...rest } = register(name);
return { ...rest, innerRef: ref };
};
...
<LabeledInput label="First Name" {...registerWithRef("firstName")} />
But the easier way is to use forwardRef with your custom component:
import { forwardRef } from 'react';
const LabeledInput = forwardRef(({ label, ...props }, ref) => (
<label>
{label}
<input ref={ref} {...props} />
</label>
));
// add display name for debugging messages and avoid eslint error
LabeledInput.displayName = "LabeledInput";
...
// works like the native <input />
<LabeledInput label="First Name" {...register("firstName")} />
That’s it! Now you have a custom input component that works just like the native one.
Another approach Link to heading
Another common approach is to pass register itself and name into the component, then register inside that component. You can also pass the errors to render error messages.
import { ErrorMessage } from "@hookform/error-message"
const LabeledInput = ({ name, register, label, errors, ...props }) => (
<label>
{label}
<input {...register(name)} {...props} />
<ErrorMessage errors={errors} name={name} />
</label>
);
...
<LabeledInput label="First Name" name="firstName" register={register} error={errors} />