# React Hook Form

> 把 chroma-panel 颜色选择器接入 React Hook Form：用 name 走非受控，或用 Controller 包裹，并说明在哪个事件上校验。

Source: https://chroma-panel.jscrate.dev/zh/react/frameworks/react-hook-form
Last updated: 2026-09-21

React 颜色选择器与 React Hook Form 可以双向配合：让 `ColorInput` 通过隐藏输入框自行提交，或者用 `<Controller>` 包裹它，让表单掌管这个值。不需要任何适配包。

## 安装

```bash
npm install chroma-panel
```

React Hook Form 不属于 chroma-panel。下面的示例假定你的应用已经装好了它，并使用 v7 的 `Controller` API。

## 非受控：让 ColorInput 自行提交

给 `ColorInput` 传一个 `name`，它就会渲染一个隐藏输入框，按你设置的 `format` 随所在表单一起提交。React Hook Form 完全看不到这个字段，所以你需要自己从表单数据中读取：

```tsx
import { useRef } from "react";
import { useForm } from "react-hook-form";
import { ColorInput } from "chroma-panel";

type Values = { label: string };

export function BrandForm() {
  const formRef = useRef<HTMLFormElement>(null);
  const { register, handleSubmit } = useForm<Values>({
    defaultValues: { label: "" },
  });

  const onSubmit = (values: Values): void => {
    const form = formRef.current;

    if (form === null) {
      return;
    }

    const brand = String(new FormData(form).get("brand") ?? "");

    save({ label: values.label, brand });
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit(onSubmit)}>
      <input {...register("label")} />
      <ColorInput
        name="brand"
        defaultValue="#3366cc"
        aria-label="Brand color"
      />
      <button type="submit">Save</button>
    </form>
  );
}
```

两种方式中，这种代码更少。代价是颜色不在表单状态里：它不会出现在 `formState`、`watch` 或 resolver 中，`reset()` 也不会动它。不过 `form.reset()` 仍会把控件恢复为 `defaultValue`，因为这是浏览器的原生行为。如果颜色除了 `required` 之外不需要其他校验，就选这种方式，`ColorInput` 原生支持 `required`，详见[表单](https://chroma-panel.jscrate.dev/zh/react/handbook/forms)。

## Controller：让表单掌管这个值

用 `<Controller>` 包裹 `ColorInput`，它就成了一个普通的受控字段。请自己传入 `value` 和 `onChange`，不要直接展开 `field`：

```tsx
import { Controller, useForm } from "react-hook-form";
import { ColorInput, type ColorChangeResult } from "chroma-panel";

type Values = { brand: string };

export function BrandForm() {
  const { control, handleSubmit } = useForm<Values>({
    defaultValues: { brand: "#3366cc" },
  });

  return (
    <form onSubmit={handleSubmit((values) => save(values))}>
      <Controller
        name="brand"
        control={control}
        rules={{ required: "Pick a color" }}
        render={({ field, fieldState }) => (
          <div>
            <ColorInput
              value={field.value}
              onChange={(result: ColorChangeResult) =>
                field.onChange(result.hex)
              }
              onChangeComplete={() => field.onBlur()}
              aria-label="Brand color"
            />
            {fieldState.error && <p role="alert">{fieldState.error.message}</p>}
          </div>
        )}
      />
      <button type="submit">Save</button>
    </form>
  );
}
```

有两点需要注意：

- **不要把 `{...field}` 展开到 `ColorInput` 上。** 这样 `field.onChange` 收到的会是一个颜色对象，而不是你想保存的字符串；`field.ref` 也挂不上去，因为 `ColorInput` 不接受 `ref`。
- **这里不要给 `ColorInput` 设置 `name`。** 设置了 `name`，它就会渲染自己的隐藏输入框，这样除了表单里已有的值，颜色还会再提交一次。

`field.value` 是字符串，正是 `ColorInput` 需要的类型。它也接受 `Hsva` 对象，如果你不想让颜色经过十六进制的取整再保存，可以用它，两者的取舍见[受控与非受控](https://chroma-panel.jscrate.dev/zh/react/handbook/controlled)。

## 在哪个事件上校验

拖动面板时，`onChange` 会持续触发；`onChangeComplete` 只在松手时触发一次。这个区别决定了你的校验模式会怎样表现：

| `useForm` 模式       | 效果                               |
| -------------------- | ---------------------------------- |
| `"onSubmit"`（默认） | 提交时校验一次，无需调整。         |
| `"onBlur"`           | 在你调用 `field.onBlur()` 时校验。 |
| `"onChange"`         | 拖动的每一帧都会重新校验。         |

上面的示例在 `onChangeComplete` 中调用 `field.onBlur()`，所以在 `mode: "onBlur"` 下，每次拖动只校验一次，而不是每秒六十次。如果你用的是 `mode: "onChange"`，而且表单感觉卡顿，首先就该改这里。

`onChange` 和 `onChangeComplete` 都和其他 prop 一起列在 [ColorInput](https://chroma-panel.jscrate.dev/zh/react/components/color-input) 中。
