見出し画像

【React×Typescript】useFormで動的なフォームを作成する

おつかれさまです🌞
アイドルに課金するために働いているなりたです。

この記事では以前
Reactで動的な入力フォームを実装しようとして悩んだことがあるのですが
それを解決した時の話を備忘録として書き残そうと思います📝


静的フォーム

それまで実装したことのある入力フォームは
以下のように入力項目が固定のもの、つまり静的なフォームでした。

サンプルソース

/**
 * 従来モデル
 */
type PersonInput = {
  name: string;
  address: string;
  nearestStation: string;
}

function App() {
  const { register, handleSubmit } = useForm<PersonInput>();
  
  const onSubmit = (data: PersonInput) => {
    console.log(data);
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)} style={{ textAlign: "center" }}>
      <h1>{"氏名"}</h1>
      <input
        placeholder={"ツーワン 太郎"}
        {...register(`name`)}
      />
      <h1>{"住所"}</h1>
      <input
        placeholder={"東京都品川区東五反田 2-1-1"}
        {...register(`address`)}
      />
      <h1>{"氏名"}</h1>
      <input
        placeholder={"五反田駅"}
        {...register(`nearestStation`)}
      />
      <p>
        <button>送信</button>
      </p>
    </form>
  );
}

export default App;

画面イメージ

フォーム送信時に取得できるobject

{
    "name": "成田 空港",
    "address": "千葉県成田市古込 1-1",
    "nearestStation": "成田空港駅"
}

動的フォーム

実装したい入力フォームは
入力項目を設定画面で追加/削除することができる。
しかも、入力項目はカテゴリ分けされている。

イメージとしては以下のような画面です。

画面イメージ

DBからは以下のようなデータを取得する想定です。

/**
 * サーバー側モデル
 */
type InputItemData = {
  /**
   *  カテゴリ名
   */
  categoryName: string;
  /**
   * 項目詳細
   */
  detail: InputItemDetail[];
};

type InputItemDetail = {
  /**
   * 一意ID
   */
  id: string;
  /**
   * 項目名(ラベルに表示)
   */
  labelName: string;
  /**
   * プレースホルダー
   */
  placeholder?: string;
};

/**
 * サンプルデータ
 */
const sampleData: InputItemData[] = [
  {
    categoryName: "個人情報",
    detail: [
      {
        id: "input-1-1",
        labelName: "氏名",
        placeholder: "ツーワン 太郎"
      },
      {
        id: "input-1-2",
        labelName: "住所",
        placeholder: "東京都品川区東五反田 2-1-1"
      },
      {
        id: "input-1-3",
        labelName: "最寄り駅",
        placeholder: "五反田駅"
      }
    ]
  },
  {
    categoryName: "会社情報",
    detail: [
      {
        id: "input-2-1",
        labelName: "会社名",
        placeholder: "株式会社 システムツー・ワン"
      },
      {
        id: "input-2-2",
        labelName: "会社住所",
        placeholder: "東京都新宿区西早稲田 2-20-15"
      },
      {
        id: "input-2-3",
        labelName: "最寄り駅",
        placeholder: "西早稲田駅"
      },
      {
        id: "input-2-4",
        labelName: "備考"
      }
    ]
  }
];

配列で来る入力項目の情報をどう捌くか最初は想像がつかなかったのですが
クライアント側のモデルも多重配列にすることにより
入力項目の情報に沿ったフォーム作成をすることができました。

サンプルソース

/**
 * クライアント側モデル
 */
type InputItems = {
  /**
   * 入力内容のリスト
   */
  InputList: InputItem[][];
};

type InputItem = {
  /**
   * 一意
   */
  id: string;
  /**
   * 入力内容
   */
  value: string;
};

function App() {
  const { register, handleSubmit } = useForm<InputItems>();
  
  const onSubmit = (data: InputItems) => {
    console.log(data);
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)} style={{ textAlign: "center" }}>
      {sampleData.map((category, idx) => {
        return(
          <div key={category.categoryName}>
            <h1>{category.categoryName}</h1>
            {
              category.detail.map((item, itemIdx) => {
                return (
                  <div key={item.id}>
                    <h2>{item.labelName}</h2>
                    <input
                      type="hidden"
                      value={item.id}
                      {...register(`InputList.${idx}.${itemIdx}.id`)}
                    />
                    <input
                      placeholder={item.placeholder ?? "未入力"}
                      {...register(`InputList.${idx}.${itemIdx}.value`)}
                    />
                  </div>
                );
              })
            }
          </div>
        );
      })}
      <p>
        <button>送信</button>
      </p>
    </form>
  );
}

export default App;

これで最初にイメージしていた画面を実現することができました👏

ちなみにこのフォームに以下のようにテキストを入力すると…

フォーム送信時に取得できるobject

フォーム送信時に取得できるobjectは以下のようになります。

{
    "InputList": [
        [
            {
                "id": "input-1-1",
                "value": "個人情報の氏名"
            },
            {
                "id": "input-1-2",
                "value": "個人情報の住所"
            },
            {
                "id": "input-1-3",
                "value": "個人情報の最寄り駅"
            }
        ],
        [
            {
                "id": "input-2-1",
                "value": "会社情報の会社名"
            },
            {
                "id": "input-2-2",
                "value": "会社情報の会社住所"
            },
            {
                "id": "input-2-3",
                "value": "会社情報の最寄り駅"
            },
            {
                "id": "input-2-4",
                "value": "会社情報の備考"
            }
        ]
    ]
}

一意IDと入力内容をDBに登録するようなシステムだったため
この実装でなんとか要件を満たすことができました🙌

最後に

この実装に取り組んでいた当時、検索しても全然有用な記事が見つからずとても苦労しました…
なんとか手探りで試行錯誤しつつ実装したので
この記事が同じような状況の方のお役に立てれば幸いです🌞