LoginSignup
1
0

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

Last updated at Posted at 2023-09-04

はじめに

mantine.jpeg

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

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

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

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

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

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のvalidateyupResolver(スキーマ)を呼ぶことでバリデーションルールを定義できるようです。

その@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/formyupResolver内部ソースを除くと以下のようなって、非同期に対応してないように見えました。

@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

去年くらいに対応してくれそうだったが、お蔵入りになっている。

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

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

コードベース↓


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. カスタムのonChangeonBlurをやる場合は、自身で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のコンポーネントに合わせているがもう少し型付けをしてほしいところ。

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

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

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

因みに、useFormで使えるメソッドはこちら
@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>;
}

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

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

react-hook-form + chakraUIの場合

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

mantine/formの場合

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

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

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>;
    )}
/>

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

まとめ

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0