Reactで親子関係のあるセレクトボックスをAPIからデータを取得して作り、フォームはstateで管理する。

目次

はじめに

管理画面とかで親子関係のあるセレクトボックスを実装しなくてはいけないこと、ありますよね?

以下の記事では、親子関係のあるセレクトボックスのデータをAPIから取得する形で実装してみました。
セレクトボックスの値は state で管理していましたが、フォームの入力値はサブミット時にイベントオブジェクトから取得する形にしていましたが、今回はフォームの値も state で管理しようと思います。

実物はこちら

あわせて読みたい
React Recipe Reactアプリのレシピ集

リポジトリはこちら

GitHub
GitHub - hakoratory/react-recipe Contribute to hakoratory/react-recipe development by creating an account on GitHub.

概要

今回は見た目に変更はありません。

以下のケースを想定して作成しています。

  • 必要なデータはAPIから取得する
  • データはDBに格納されている(今回は一つの配列をDBと見立てています)
  • 中項目・小項目には、一つ上の項目と紐づけるための parentId がある
  • 大項目の parentIdnull

実装

データにも変更はありませんが、再掲しておきます。

export const parentChildSelectItems: Array<ParentChildSelectItem> = [
    {
        id: 1,
        parentId: null,
        value: 'item-1'
    },
    {
        id: 2,
        parentId: null,
        value: 'item-2'
    },
    // 中項目
    {
        id: 3,
        parentId: 1,
        value: 'item-1-1'
    },
    {
        id: 4,
        parentId: 1,
        value: 'item-1-2'
    },
    {
        id: 5,
        parentId: 2,
        value: 'item-2-1'
    },
    {
        id: 6,
        parentId: 2,
        value: 'item-2-2'
    },
    // 小項目
    {
        id: 7,
        parentId: 3,
        value: 'item-1-1-1'
    },
    {
        id: 8,
        parentId: 3,
        value: 'item-1-1-2'
    },
    {
        id: 9,
        parentId: 4,
        value: 'item-1-2-1'
    },
    {
        id: 10,
        parentId: 4,
        value: 'item-1-2-2'
    },
    {
        id: 11,
        parentId: 5,
        value: 'item-2-1-1'
    },
    {
        id: 12,
        parentId: 5,
        value: 'item-2-1-2'
    },
    {
        id: 13,
        parentId: 6,
        value: 'item-2-2-1'
    },
    {
        id: 14,
        parentId: 6,
        value: 'item-2-2-2'
    },
]

以下が完成したコードです。

'use client'
import {Box, Button} from "@mui/material";
import React, {ChangeEvent, useEffect, useState} from "react";
import {parentChildSelectItems} from "@/data/data";
import {FetchItemsRequestType, ParentChildSelectItem, ParentChildSelectItemForm} from "@/types/parent-child-select"

const isUseMockData = true

const fetchItems = async (param: FetchItemsRequestType): Promise<Array<ParentChildSelectItem>> => {
  if (isUseMockData) {
    return parentChildSelectItems.filter(item => item.parentId === param.parentId)
  } else {
    const response = await fetch('/api/parent-child-select/items', {
      method: 'POST',
      body: JSON.stringify(param)
    })
    const {items} = await response.json()
    return items
  }
}

export default function ParentChildSelectApiForm() {
  const [items, setItems] = useState<Array<ParentChildSelectItem>>([])
  const [subItems, setSubItems] = useState<Array<ParentChildSelectItem>>([])
  const [subSubItems, setSubSubItems] = useState<Array<ParentChildSelectItem>>([])
  const [formData, setFormData] = useState<ParentChildSelectItemForm>({
    itemId: null,
    subItemId: null,
    subSubItemId: null,
  })

  useEffect(() => {
    (async () => {
      const newItems = await fetchItems({ parentId: null })
      setItems(newItems)
    })()
  }, [])

  const handleChangeItem = async (event: ChangeEvent<HTMLSelectElement>) => {
    const newSubItem = await fetchItems({ parentId: parseInt(event.target.value) })
    setSubItems(newSubItem)
    setSubSubItems([])

    const newFormData: ParentChildSelectItemForm = {...formData, itemId: parseInt(event.target.value)}
    setFormData(newFormData)
  }

  const handleChangeSubItem = async (event: ChangeEvent<HTMLSelectElement>) => {
    const newSubSubItem = await fetchItems({ parentId: parseInt(event.target.value) })
    setSubSubItems(newSubSubItem)

    const newFormData: ParentChildSelectItemForm = {...formData, subItemId: parseInt(event.target.value)}
    setFormData(newFormData)
  }

  const handleChangeSubSubItem = async (event: ChangeEvent<HTMLSelectElement>) => {
    const newFormData: ParentChildSelectItemForm = {...formData, subSubItemId: parseInt(event.target.value)}
    setFormData(newFormData)
  }

  const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
    event.preventDefault();
    const item = formData.itemId ? items.find(item => item.id === formData.itemId)?.value : ''
    const subItem = formData.subItemId ? subItems.find(subItem => subItem.id === formData.subItemId)?.value : ''
    const subSubItem = formData.subSubItemId ? subSubItems.find(subSubItem => subSubItem.id === formData.subSubItemId)?.value : ''
    alert(
        `item: ${item}\nsubItem: ${subItem}\nsubSubItem: ${subSubItem}`
    );
  }

  return (
    <main>
      <Box mt={2} ml={2}>
        <Button type="button" variant="contained" onClick={handleClick}>submit</Button>
        <Box mt={2}>
          <Box>
            大項目
          </Box>
          <Box>
            <select className="w-40" onChange={handleChangeItem} name="item">
              <option value=""></option>
              {
                items.map(item => (
                    <option key={item.id} value={item.id}>{item.value}</option>
                ))
              }
            </select>
          </Box>
        </Box>
        <Box mt={2}>
          <Box>
            中項目
          </Box>
          <Box>
            <select className="w-40" onChange={handleChangeSubItem} name="subItem">
              <option value=""></option>
              {
                subItems.map(subItem => (
                    <option key={subItem.id} value={subItem.id}>{subItem.value}</option>
                ))
              }
            </select>
          </Box>
        </Box>
        <Box mt={2}>
          <Box>
            小項目
          </Box>
          <Box>
            <select className="w-40" onChange={handleChangeSubSubItem} name="subSubItem">
              <option value=""></option>
              {
                subSubItems.map(subSubItem => (
                    <option key={subSubItem.id} value={subSubItem.id}>{subSubItem.value}</option>
                ))
              }
            </select>
          </Box>
        </Box>
      </Box>
      <Box mt={6} ml={2}>
        <Box>デバッグセクション</Box>
        <Box mt={2}>
          <Box>大項目の value</Box>
          <Box>
            {
              items.map(item => (
                  <Box key={item.id} className={item.id === formData.itemId ? 'font-bold' : ''}>{item.value}</Box>
                ))
            }
          </Box>
        </Box>
        <Box mt={2}>
          <Box>中項目の value</Box>
          <Box>
            {
              subItems.filter(subItem => subItem.parentId === formData.itemId)
                .map(subItem => (
                  <Box key={subItem.id} className={subItem.id === formData.subItemId ? 'font-bold' : ''}>{subItem.value}</Box>
                ))
            }
          </Box>
        </Box>
        <Box mt={2}>
          <Box>小項目の value</Box>
          <Box>
            {
              subSubItems.filter(subSubItem => subSubItem.parentId === formData.subItemId)
                .map(subSubItem => (
                  <Box key={subSubItem.id} className={subSubItem.id === formData.subSubItemId ? 'font-bold' : ''}>{subSubItem.value}</Box>
                ))
            }
          </Box>
        </Box>
      </Box>
    </main>
  );
}

各部分の解説

API呼び出し処理については前回と変わらないので割愛します。
以下記事をご覧くだださい。

state の宣言

大・中・小、各項目で選択されている値を一つの state で管理します。

  const [formData, setFormData] = useState<ParentChildSelectItemForm>({
    itemId: null,
    subItemId: null,
    subSubItemId: null,
  })

state の更新

入力値が変更されたら state を更新します。
オブジェクトなのでイミュータブルな更新でいきましょう。

  const handleChangeItem = async (event: ChangeEvent<HTMLSelectElement>) => {
    const newSubItem = await fetchItems({ parentId: parseInt(event.target.value) })
    setSubItems(newSubItem)
    setSubSubItems([])

    const newFormData: ParentChildSelectItemForm = {...formData, itemId: parseInt(event.target.value)}
    setFormData(newFormData)
  }

  const handleChangeSubItem = async (event: ChangeEvent<HTMLSelectElement>) => {
    const newSubSubItem = await fetchItems({ parentId: parseInt(event.target.value) })
    setSubSubItems(newSubSubItem)

    const newFormData: ParentChildSelectItemForm = {...formData, subItemId: parseInt(event.target.value)}
    setFormData(newFormData)
  }

  const handleChangeSubSubItem = async (event: ChangeEvent<HTMLSelectElement>) => {
    const newFormData: ParentChildSelectItemForm = {...formData, subSubItemId: parseInt(event.target.value)}
    setFormData(newFormData)
  }

以下のように1行にまとめることももちろん可能です。

setFormData({...formData, itemId: parseInt(event.target.value)})

入力値の取得

サブミットボタンが押されたら、前回はイベントオブジェクトから値を取得しましたが、今回は state から値を取得します。

  const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
    event.preventDefault();
    const item = formData.itemId ? items.find(item => item.id === formData.itemId)?.value : ''
    const subItem = formData.subItemId ? subItems.find(subItem => subItem.id === formData.subItemId)?.value : ''
    const subSubItem = formData.subSubItemId ? subSubItems.find(subSubItem => subSubItem.id === formData.subSubItemId)?.value : ''
    alert(
        `item: ${item}\nsubItem: ${subItem}\nsubSubItem: ${subSubItem}`
    );
  }

IDEの入力補完が効くので、私は state で値を管理するほうが好きですね。

おわりに

さて、いかがだったでしょうか?
今回は、セレクトボックスの値の取り方や持ち方は同じですが、入力値の持ち方が異なるバージョンのご紹介でした。

Reactレシピシリーズでは、「こんなUIを作ってみてほしい」「こんな場合はどうすれば?」といったアイディアを募集中です!
お気軽にコメントください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

28歳のときに音楽家からエンジニアに転向。
WEB系のシステム開発(Java, C#, Vue, React など)に携わっています。

自分らしく力を発揮できて、自信を持って生きられる人を増やすための活動をしています。

コメント

コメントする

目次