mantine/formを使ってみてわかった盲点３選


# はじめに

![mantine.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924294/f85d5635-9157-5195-3d6f-db99655db5bc.jpeg)


初めてフロント管理画面の開発で`mantine/ui`（かみなり4倍ダメージ）を使い、mantineにformライブラリも備わっていたので**mantine/form**のhooksを使ってみました。

過去に筆者は`react-hook-form`を使っていたので、`mantine/form`と比較して良かったところや使いづらいところなど、まとめました。


※ 2023/09時点： mantine/form: v6.0.19 


https://mantine.dev/form/use-form/

### Mantine UIライブラリについて

100種類以上のカスタマイズ可能なコンポーネントやhooksを備えているUIライブラリ。

### なぜmantine/formを採用したか

今回初めて、mantine/uiを使いformのAPIがあったので互換性があり、yup（バリデーションスキーマ）の対応、`react-hook-form`と見たところ使用感が似ていたので今回使ってみました。




## 1. 非同期のバリデーションに対応していない

まず要件として、画像ファイルのアップロードの画像解像度（縦、横）のバリデーションをフロントで行う必要があった。

画像のアップロードフォームを実装する際に、画像の解像度サイズをチェックしたい場合、`image.onLoad()`や`file.onload()`などのPromiseベースでチェックをかけたい時に筆者は`yup`を使って非同期のバリデーションを含んだスキーマ定義を行い、いざ使うとしたところmantine/formは対応できていなかった。

実際に出たエラー

>`yup-resolver.ts:10 Uncaught TypeError: Cannot read properties of undefined (reading 'forEach') at yup-resolver.ts:10:22`



```ts: エラー時のバリデーションスキーマ

export const formSchema: yup.ObjectSchema<FormValues> = yup.object({
  name: yup.string().max(100).required(),
  file: yup
    .mixed<File>()
    .required()
    .test('file', エラーメッセージ, (value) => value instanceof File)
    .test({
      name: 'fileResolution',
      message: エラーメッセージ,
      async (value: File) => {
        // valueを元に`image.onLoad`で画像のheight, widthを取得チェック。
        return await fileResolution(value)
      },
    }),
    .test('fileByteSize', エラーメッセージ, (value) => value.size <= 　1000),
});

```

上記の形のyupをmantine/formの`yupResolver()`に読み込ませるとえらーになりました。



結論： useFormのvalidateにyup（バリデーションスキーマ）を読み込む`yupResolver`のメソッドを使って、`validate`に読み込ませるのだが**内部的に対応していなかった**。



因みにreact-hook-formでは、もちろん対応していて内部的に違うのかみてみました。
react-hook-formでyup（validate schema）を使う時は、`@hookform`から`yupResolver`を使うようになっており、mantine同様、useFormの`validate`に`yupResolver（スキーマ）`を呼ぶことでバリデーションルールを定義できるようです。


その`@hookform`の内部を見てみるとデフォルトで非同期のバリデーションをかけるようにしていました。

```@hookform/**/yup-resolver.d.ts
export declare function yupResolver<TFieldValues extends FieldValues>(schema: Yup.ObjectSchema<TFieldValues>, schemaOptions?: Parameters<(typeof schema)['validate']>[1], resolverOptions?: {
    /**
     * @default async
     */
    mode?: 'async' | 'sync';
    /**
     * Return the raw input values rather than the parsed values.
     * @default false
     */
    raw?: boolean;
}): Resolver<TFieldValues>;

```


対して`mantine/form`の`yupResolver`内部ソースを除くと以下のようなって、非同期に対応してないように見えました。

```@mantine/form/**/yup-resolver.d.ts
import type { FormErrors } from '../../types';
export declare function yupResolver(schema: any): (values: Record<string, any>) => FormErrors;
//# sourceMappingURL=yup-resolver.d.ts.map
```

去年くらいに対応してくれそうだったが、お蔵入りになっている。
- https://github.com/mantinedev/mantine/pull/1255


### 結局このバリデーションをどうしたか

画像解像度のチェックの非同期バリデーションをかけるため専用の`value（例：imageResolution）`を追加し、ファイルアップロードしたタイミングで`imageResolution`に画像解像度を保存させバリデーションをかけるようにしました。
（フォームとして`imageResolution`は表示させてないです。）

コードベース↓

```ts

export type FormValues = {
  name: string;
  file: File | null;
  imageResolution?: {
    width: number;
    height: number;
  };
};

const initialValues: FormValues = {
    name: '',
    file:  null,
    imageResolution: undefined,
  }

export const formSchema: yup.ObjectSchema<FormValues> = yup.object({
  name: yup.string().max(100).required(),
  file: yup
    .mixed<File>()
    .required()
    .test('file', エラーメッセージ, (value) => value instanceof File)
    .test('fileByteSize', エラーメッセージ, (value) => value.size <= 　1000),
  imageResolution: yup
    .object()
    .shape({
      width: yup.number().required(),
      height: yup.number().required(),
    })
    .test({
      name: 'fileResolution',
      message: エラーメッセージ,
      test({ width, height }, { parent }) {
        const formValue: FormValues = parent;
        if () {
        // ここで画像解像度チェック
        }
        return true;
      },
    }),
});

const { getInputProps, onSubmit, isDirty, errors, values, setValues } =
    useForm<FormValues>({
      validateInputOnBlur: true,
      initialValues,
      validate: yupResolver(formSchema),
    });

// 
const handleEvent = useCallback(
    async (file: File) => {
      // 画像解像度の読み込み
      const { width, height } = await loadImage(file);
      // 取得できたらセット
      setValues({ file, imageResolution: { width, height } });
    },
    [setValues]
  );

```



## 2. カスタムの`onChange`や`onBlur`をやる場合は、自身でvalueを更新する必要がある


react-hook-formの`register`メソッドには、optionでonChangeやonBlueがあるのでカスタムのイベントをそのoptionに渡せば、valueの更新とは違う処理が追加できる。

それに対して、mantine/formの`getInputProps`はそのoption引数がなく、現状inputのtypeを`input or checkbox`にするかgetInputPropsに含まれるerrorプロパティを受け取るか、などの設定しかできないようです。

> 参照：https://mantine.dev/form/use-form/#getinputprops
>As second parameter options can be passed.
> ・type: default input. Needs to be configured to checkbox if input requires checked to be set instead of value.
・withError: default type === 'input'. Specifies if the returned object contains an error property with form.errors[path] value.
・withFocus: default true. Specifies if the returned object contains an onFocus handler. If disabled, the touched state of the form can only be used if all values are set with setFieldValue.

ソースコード（getInputPropsの型↓）

```@mantine/form/**/GetInputProps.ts
export type GetInputProps<Values> = <Field extends LooseKeys<Values>>(path: Field, options?: {
    type?: GetInputPropsType;
    withError?: boolean;
    withFocus?: boolean;
}) => {
    value: any;
    onChange: any;
    checked?: any;
    error?: any;
    onFocus?: any;
    onBlur?: any;
};
```
返されるobjectもmantine/UIのコンポーネントに合わせているがもう少し型付けをしてほしいところ。

なので、`onChange`や`onBlur`などのカスタムイベントをする場合は、自身で`setFieldValues`もしくは`setValues`でinput元のvalue値を更新してあげる必要がありました。

## 3. フォームのレンダリング制御のメソッドがない

react-hook-formには`useWatch()`を使うことで、コンポーネントの再描画の範囲を制御することが可能だが、`mantine/form`にはそういったメソッドや制御を備えているわけではなく、一つの入力フォームをレンダリングすると全体の入力フォームが再描画してしまうのでフォームの数が多い場合は再描画の注意が必要そうです。



<details><summary>因みに、useFormで使えるメソッドはこちら</summary><div>




```ts: @mantine/form/**/types.d.ts
export interface UseFormReturnType<Values, TransformValues extends _TransformValues<Values> = (values: Values) => Values> {
    values: Values;
    errors: FormErrors;
    setValues: SetValues<Values>;
    setErrors: SetErrors;
    setFieldValue: SetFieldValue<Values>;
    setFieldError: SetFieldError<Values>;
    clearFieldError: ClearFieldError;
    clearErrors: ClearErrors;
    reset: Reset;
    validate: Validate;
    validateField: ValidateField<Values>;
    reorderListItem: ReorderListItem<Values>;
    removeListItem: RemoveListItem<Values>;
    insertListItem: InsertListItem<Values>;
    getInputProps: GetInputProps<Values>;
    onSubmit: OnSubmit<Values, TransformValues>;
    onReset: OnReset;
    isDirty: GetFieldStatus<Values>;
    isTouched: GetFieldStatus<Values>;
    setTouched: SetFormStatus;
    setDirty: SetFormStatus;
    resetTouched: ResetStatus;
    resetDirty: ResetDirty<Values>;
    isValid: IsValid<Values>;
    getTransformedValues: GetTransformedValues<Values, TransformValues>;
}

```

</div></details>

https://mantine.dev/form/values/



### `mantine/ui`だからformの良いところ

バリデーションエラーのメッセージを表示させるテキストを用意しなくてもmantineのカスタムコンポーネントの中に`error`propsが内蔵されているので、`{...getInputProps()}`を渡すだけで簡潔できる。

```ts: react-hook-form + chakraUIの場合

    <Input {...register("name", {required: {value: true, message: "nameError"}})} />
    {errors.name && errors.name.message} // 定義する必要がある。

```

```ts: mantine/formの場合

 <TextInput
    {...form.getInputProps('name')}
 />

```

カスタムコンポーネントの作りによると思うが、例えばreact-selectやchakra uiの`Select`コンポーネントをreact-hook-formと組み合わせる時は`Controller`を使わないといけない事に対して、mantine UIは`select`コンポーネントも以下のように渡すことができるので、簡潔に済みました。

```ts: react-hook-form + chakraUIの場合

<Controller
    control={control}
    name="food"
    rules={{ required: "Please enter at least one food group." }}
    render={({
        field: { onChange, onBlur, value, name, ref },
        fieldState: { error }
    }) => (
     <FormControl py={4} isInvalid={!!error} id="food">
      <FormLabel>Food Groups</FormLabel>
      <Select
        isMulti
        name={name}
        ref={ref}
        onChange={onChange}
        onBlur={onBlur}
        value={value}
        options={foodGroups}
        placeholder="Food Groups"
        closeMenuOnSelect={false}
      />
      <FormErrorMessage>{error && error.message}</FormErrorMessage>
    </FormControl>;
    )}
/>

```

```ts: mantine/formの場合
<Select
  label="オプション"
  data={options}
  {...getInputProps('options')}
/>
```

### まとめ

以上がmantine/formを数ヶ月使ってみて、mantine/uiを使っていく中でシンプルなフォームをを作成する場合には開発効率には問題ないと思いますが、バリデーションを非同期で行いたい場合やレンダリング制御の考慮も必要な場合は他のライブラリも検討してみると良いかもしれません。