カスタムフックで作る範囲選択可能なチェックボックス

2024/11/05に公開

はじめに

こんにちは。レンティオでエンジニアをしている小島です。

レンティオでは一部の管理画面を React (Next.js) で作っています。

一覧系の画面を作るとき、特定のアイテムをチェックボックスで選択して何かアクションを実行したいことがあると思います。AWS コンソールでよく見ますね。

さらに、1つずつ選択するのが面倒なので Excel のように Shift キーを押しながら範囲選択したくなりました。そのやり方を残しておこうと思います。

動作イメージ

始点にチェックを入れた後、Shift キーを押しながら終点をクリックするとその範囲すべてにチェックを入れます。チェックを外す操作も同様です。

キャプチャ

カスタムフックで実装

いろいろな一覧画面で使える機能なのでカスタムフックとして使い回せる形で実装します。
配列操作が多く読みづらくなりますが、やっていることはコメントの通りです。

import { useCallback, useEffect, useState } from "react"

export const useCheckboxes = <T>(items: T[]) => {
  // チェックを入れているアイテムを保持
  const [checkedItems, setCheckedItems] = useState<T[]>([])
  // 始点のアイテムを保持
  const [startItem, setStartItem] = useState<T>()

  useEffect(() => {
    setCheckedItems([])
    setStartItem(undefined)
  }, [items])

  const onClickCheckbox = useCallback((item: T, shiftKey: boolean) => {
    if (shiftKey && startItem) {
      // Shift キーを押しながらの終点クリック
      // 範囲内のアイテムを始点のチェック状態と揃える
      const [minIndex, maxIndex] = [items.indexOf(item), items.indexOf(startItem)].sort((a, b) => a - b)
      const targetItems = items.slice(minIndex, maxIndex + 1)

      if (checkedItems.includes(startItem)) {
        setCheckedItems(Array.from(new Set([...checkedItems, ...targetItems])))
      } else {
        setCheckedItems(checkedItems.filter((i) => !targetItems.includes(i)))
      }
    } else {
      // 始点クリック
      // 単純にそのアイテムのチェック状態を反転
      if (checkedItems.includes(item)) {
        setCheckedItems(checkedItems.filter((i) => i !== item))
      } else {
        setCheckedItems([...checkedItems, item])
      }
      setStartItem(item)
    }
  }, [checkedItems, startItem])

  return [checkedItems, onClickCheckbox] as [typeof checkedItems, typeof onClickCheckbox]
}

使う側

Shift キーが押されているかを知るために onClick を使う必要があります。

const StaffsPage: NextPage<YourProps> = ({ staffs }) => {
  const [checkedStaffs, onClickCheckbox] = useCheckboxes(staffs)

  return (
    <div>
      <ul>
        {staffs.map((staff) => (
          <li key={staff.email}>
            <input
              type="checkbox"
              checked={checkedStaffs.includes(staff)}
              onClick={(e) => onClickCheckbox(staff, e.shiftKey)}
              onChange={() => {}}
            />
            {staff.email}
          </li>
        ))}
      </ul>
      <p>{checkedStaffs.map((staff) => staff.email).join(", ")}</p>
    </div>
  )
}

おわりに

以上、検索してもあまり例がないようでしたので書き残してみました。
使う側はシンプルに記述できるのでなかなか助かっています。

採用情報

レンティオではエンジニアを募集しています。もし興味をお持ちいただけたらこちらもお目通しいただけると幸いです。

https://recruit.rentio.co.jp/engineer
https://www.rentio.jp/

Discussion