# 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
* To support schema-based form validation with [Yup](https://github.com/jquense/yup), [Zod](https://github.com/vriad/zod) , [Superstruct](https://github.com/ianstormtaylor/superstruct) & [Joi](https://github.com/hapijs/joi), where you can pass your `schema` to [useForm](https://www.react-hook-form.com/api#useForm) as an optional config. It will validate your input data against the schema and return with either [errors](https://www.react-hook-form.com/api#errors) or a valid result.

```tsx
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&#x20;
* `control`is passed into component with provider by default and linked with the parent form, so that the component becomes controllable
* `useController`return the field status (isDirty, error) and on change method, etc

```tsx
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 handle
* Return the values of array and append, remove method

```tsx
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;
```

```tsx
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>
  );
};

```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://petercheng7788.gitbook.io/developer-note/frontend/react/form-and-validation/react-hook-form.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
