LoginSignup
1
0

MUI + React-Hook-Form + Zod で選択肢が動的に変化するフォームの作成

Posted at

MUI + React-Hook-Form + Zod で動的に選択肢が変化するフォームの作成

コードは以下のrepositoryにおいています。

使用したライブラリのバージョンは以下の通りです。

  • swr: 2.2.4
  • zod: 3.22.4
  • react-hook-form: 7.49.2
  • next: 14.0.4
  • axios: 1.6.5

背景と目的

フォームを作成するのに、以下の観点からZodReact-Hook-FormMUI を使用することはよくあると思います。

  • バリデーションのためにZodを使用したい。
  • フォームの状態管理やデータのバリデーション実行をReact-Hook-Formに任せたい。
  • いい感じのデザインを考えるのが面倒なため、MUIを使用したい。
  • APIのキャッシュや競合を防ぐため、useSWRを用いたい。

この記事では、これらの技術スタックにおいて、動的に選択肢が変化するフォームを作成したので共有します。
動的に選択肢が変化するというのは、1つ前の項目の入力内容に応じて、次の項目の入力の選択肢が変化するというものです。
次に具体的な成果物について説明します。

作るフォームの内容

成果物は以下のような3つの項目があるフォームです。それぞれの項目は必須のフォームです。

image.png

2つめの Repository は1つめの User Name に応じた選択肢が取得され、選択可能となります。もし、Repository がない User Name を指定した場合は No Itemとなります。 User NameRepository の値をもとにバックエンドに問い合わせ、選択肢を取得します。

image.png

image.png

同様に、3つめの File は 1, 2つめの User Name, Repository に応じて選択肢が取得されます。もし存在しないUser Name, Repository を指定した場合は同様に No Item となります。

image.png

また、フォームの各項目を埋めたあとに、依存先の項目の内容が修正された場合、依存していた項目の選択を解除します。例えば、以下は、上画像からUser Nameを削除した場合の結果です。RepositoryFileはそれぞれUser Nameに依存していたため、そのUser Nameが変更された場合は選択肢を解除します。
image.png

選択肢を動的に変化させるには

ここでは、依存関係がある RepositoryFileの選択肢のコンポーネント SelectField について解説します。

src/components/SelectField/index.tsx
'use client';

import React from 'react';
import { useController, UseControllerProps } from 'react-hook-form';
import {
  Select,
  MenuItem,
  FormControl,
  InputLabel,
  SelectChangeEvent,
  FormHelperText,
} from '@mui/material';
import { type FormType } from '@/schema/form';

type SelectFieldProps = UseControllerProps<FormType> & {
  label: string;
  options: { label: string; value: string }[]; // 動的に変化する選択肢。選択肢は親コンポーネントのフォームで取得する(後述)
  required?: boolean;
  initValue?: boolean;
  errorMessage?: string;
  onChangePre?: (e: SelectChangeEvent<string>) => void; // 依存しているフォームの初期化をするためのイベントリスナー(後述)
};

export default function SelectField({
  label,
  required,
  options,
  initValue,
  errorMessage,
  ...props
}: SelectFieldProps) {
  const { field } = useController(props);
  return (
    <FormControl fullWidth required={required ? true : false}>
      <InputLabel>{label}</InputLabel>
      <Select
        {...field}
        label={label}
        onChange={(e) => {
          props.onChangePre && props.onChangePre(e);
          field.onChange(e);
        }}
      >
        {options.map((option) => (
          <MenuItem key={option.value} value={option.value}>
            {option.label}
          </MenuItem>
        ))}
      </Select>
      {errorMessage && <FormHelperText error>{errorMessage}</FormHelperText>}
    </FormControl>
  );
}

フォームの依存関係は、 SelectField を持つ親コンポーネント Form で管理しています。
例えば、1つめのUser Nameの値が変わると、2, 3つめのRepository, Fileの選択肢が変化するため、それらで現在設定している値を空文字列(=選択なし)に設定しています(以下が該当部分)。選択している値はreact-hook-formで管理しているため、setValueを用いて初期化しています。
上記の処理を行うために、コンポーネントのpropsにはonChangePreを渡しています。次に onChangePre を渡されたあとの処理を説明します。

<TextField
  label="User Name"
  required
  control={useFormMethod.control}
  name="user"
  onChangePre={(e) => {
    useFormMethod.setValue('repository', '');
    useFormMethod.setValue('file', '');
  }}
/>

以下のように、onChangePreMUITextFieldコンポーネントに渡され、onChangeとして設定しています。 field.onChange(e) はreact-hook-form のmethodであり、この項目の入力された値を設定するためのイベントリスナーになります。

export default function TextField({
  label,
  required,
  onChangePre,
  ...props
}: UseControllerProps<FormType> & {
  label: string;
  required?: boolean;
  onChangePre?: (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => void;
}) {
  const { field } = useController(props);

  return (
    <MUITextField
      {...field}
      label={label}
      required={required ? true : false}
      onChange={(e) => {
        onChangePre && onChangePre(e); // このコンポーネントの入力を変更する前に依存しているコンポーネントの値を初期化する
        field.onChange(e);
      }}
    />
  );
}

1つめ、2つめの選択肢の依存に応じてそれ以降の項目の選択肢を動的に変化させるために、react-hook-formのwatchを用いて変数を購読しています。これにより、入力が変化するたびにAPIがコールされ、動的に選択肢が変化します。

const repository = useRepositoryOptions(useFormMethod.watch('user'));
const file = useFileOptions(useFormMethod.watch('repository'));

おわりに

1つ前の選択肢に依存したフォームを作成しようと思ったときにサンプルコードが落ちていなかったため、今回は簡単に作成したフォームを共有しました。

ただ、今のコードの場合は以下の点が問題です。

  • TextFieldなど、入力が開始してから完了するまで時間がかかる項目に関しては、その都度APIを呼びに行くのは非効率。
    • useDebounce を用いて一定待ち時間を確保すると良さそうです。
  • 親コンポーネントで依存関係を管理しているため、変更点のないコンポーネントも再レンダリングされる
    • Memo化を行うことで解消することができます。しかし、Meomo化を行うとコードが複雑になるため、パフォーマンスの問題があってから対応でも良いと思います。

参考記事

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