React Hook Form
Form
Mainly responsible for defining form submitting function, the type of form data, validation rule of each component, pass the props to component through form context provider
List of validation rules supported:
required
min
max
minLength
maxLength
pattern => regExp
validate => using self-defined function to validate
import React from "react";
import { FormProvider, useForm } from "react-hook-form";
import { Form } from "react-aria-components";
import CustomTextField from "../forms/customTextField";
import CustomButton from "../forms/customButton";
import useLoginMutation from "../../hooks/login/useLoginMutation";
import { useNavigate } from "@tanstack/react-router";
import { zodResolver } from "@hookform/resolvers/zod";
import { LoginRequest, loginRequestSchema } from "../../types/login";
const defaultFormValues: LoginRequest = {
email: "",
password: "",
};
const Login = () => {
const { mutateAsync } = useLoginMutation();
const navigate = useNavigate();
const methods = useForm({
defaultValues: defaultFormValues,
resolver: zodResolver(loginRequestSchema),
});
const { handleSubmit, setError } = methods;
const onSubmit = async (data: LoginRequest) => {
try {
const user = await mutateAsync(data);
localStorage.setItem("user", user.id.toString());
navigate({ to: "/" });
} catch (err) {
if (err instanceof Error) {
// set back the error to the field
setError("email", { message: err.message });
}
}
}
// to have side effect when failed to submit
const onError = (input: unknown) => {
console.log(input);
};
return (
<FormProvider {...methods}>
<Form
className="h-full flex flex-col items-center justify-center gap-2"
onSubmit={(e) => {
e.preventDefault();
// for handle submit, the validation will be triggered when the data is
// firstly submitted or changed
handleSubmit(onSubmit, onError)();
}}
>
<CustomTextField
name="email"
rules={{
required: true,
}}
label={"Email"}
/>
<CustomTextField
name="password"
type="password"
rules={{ required: true }}
label={"Password"}
/>
<CustomButton label="Login" type="submit" className="mt-5" />
</Form>
</FormProvider>
);
};
export default Login;
Component
Component is mainly responsible to handle field status correctly and the change behaviour
control
is passed into component with provider by default and linked with the parent form, so that the component becomes controllableuseController
return the field status (isDirty, error) and on change method, etc
import { Label, TextField, Input, FieldError } from "react-aria-components";
import { FieldValues, useController } from "react-hook-form";
export default function CustomTextField({
label,
control,
name,
rules,
defaultValue,
...props
}: FieldValues) {
const { field, fieldState } = useController({
name,
control,
rules,
defaultValue,
});
return (
<TextField
isInvalid={!!fieldState.error}
{...field}
{...props}
className="flex flex-col gap-2"
>
<Label className="text-xl">{label}</Label>
<Input className="h-9 w-80 bg-slate-500 text-lg rounded-sm text-white" />
<FieldError className={"text-red-400"}>
{fieldState.error?.message
? fieldState.error.message
: "The field is required"}
</FieldError>
</TextField>
);
}
For array of field case, using
useFieldArray
hook to handleReturn the values of array and append, remove method
import {
FieldValues,
useController,
useFieldArray,
useFormContext,
} from "react-hook-form";
import CustomSwitch from "./customSwitch";
import { Option } from "../../types/common";
interface Props extends FieldValues {
options: Option[];
isReadOnly?: boolean;
}
const CustomSwitchGroup = ({
control,
name,
options,
isReadOnly = false,
}: Props) => {
const { remove, append } = useFieldArray({
name,
control,
});
const { watch, getValues } = useFormContext();
const { fieldState } = useController({
name,
control,
});
return (
<>
{options.map((option) => (
<CustomSwitch
isSelected={watch(name).includes(option.key)}
onChange={(value) => {
const optionsSelected: number[] = getValues(name);
if (!value) {
const index = optionsSelected.findIndex(
(optionKey) => optionKey === option.key,
);
remove(index);
} else {
if (!optionsSelected.includes(option.key)) {
append(option.key);
}
}
}}
label={option.name}
key={option.key}
isReadOnly={isReadOnly}
/>
))}
{fieldState.error && (
<span className={"text-red-400"}>
{fieldState.error?.message
? fieldState.error.message
: "The field is required"}
</span>
)}
</>
);
};
export default CustomSwitchGroup;
import * as React from "react";
import { useForm, useFieldArray, useWatch } from "react-hook-form";
export default function App() {
const { control, handleSubmit } = useForm();
const { fields, append, update } = useFieldArray({
control,
name: 'array'
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
{fields.map((field, index) => (
<Edit
key={field.id}
control={control}
update={update}
index={index}
value={field}
/>
))}
<button
type="button"
onClick={() => {
append({ firstName: "" });
}}
>
append
</button>
<input type="submit" />
</form>
);
}
const Display = ({ control, index }) => {
const data = useWatch({
control,
name: `array.${index}`
});
return <p>{data?.firstName}</p>;
};
const Edit = ({ update, index, value, control }) => {
const { register, handleSubmit } = useForm({
defaultValues: value
});
return (
<div>
<Display control={control} index={index} />
<input
placeholder="first name"
{...register(`firstName`, { required: true })}
/>
<button
type="button"
onClick={handleSubmit((data) => update(index, data))}
>
Submit
</button>
</div>
);
};
Last updated
Was this helpful?