Route360

Reactでアイテムを絞り込み(マルチフィルター)を行う方法

目次

EC・ショッピングアプリなどで、グループや種類による複数条件での絞り込みをReactで行う方法です。

今回は例として、プロダクト毎にタグをつけ、タグで絞り込みができるようにします。

今回利用するReact HookはuseState()となります。

動作環境:

  • Node.js v18.12.1
  • React v18.2.0

元となるリストを生成

今回は、以下のようなリストを作りました。

const DATA = [
  {
    id: 1,
    title: "Enjoy studying English",
    tags: [
      {
        id: "tag1",
        title: "English",
        slug: "english",
      },
      {
        id: "tag2",
        title: "For kids",
        slug: "kids",
      },
    ],
  },
  {
    id: 2,
    title: "Parlons français",
    tags: [
      {
        id: "tag3",
        title: "French",
        slug: "french",
      },
      { id: "tag2", title: "Kids", slug: "kids" },
    ],
  },
  {
    id: 3,
    title: "Intermediate English",
    tags: [
      {
        id: "tag1",
        title: "English",
        slug: "english",
      },
      {
        id: "tag4",
        title: "Adults",
        slug: "adults",
      },
    ],
  },
  {
    id: 4,
    title: "How to study French",
    tags: [
      {
        id: "tag3",
        title: "French",
        slug: "french",
      },
      {
        id: "tag4",
        title: "Adults",
        slug: "adults",
      },
    ],
  },
]

今回は上記のようにハードコーディングしていますが、通常の現場ではNext.jsやGatsby.js等によりすべてのアイテムをmap()で展開するパターンとなると思います。

このconst DATAは後ほどreturn内でmap()展開するため、key指定のためのidを設定しています。

絞り込みフィルターを通したリストを生成

用意したconst DATAをそのまま表示しても絞り込みはできないため、フィルターを通した「整形可能な配列データ」に変換させます。

今回はタグを利用して絞り込みをするため、"ユーザーによって選択されたタグ"を含むアイテムのみ展開するようにします。

const [filterTags, setFilterTags] = useState([])

const filteredDATA = DATA.filter(node =>
  filterTags.length > 0
    ? filterTags.every(filterTag =>
        node.tags.map(tag => tag.slug).includes(filterTag)
      )
    : DATA
)

何かしらのタグが選択されている場合(= filterTags.length > 0)では、選択されているタグが含まれるアイテムのみをフィルタリング。タグが一切選択されていない時は、初期状態であるDATAを返します。

ここでのポイントは、every()の利用です。選択されたタグを持っているアイテムのみをここで絞り込みます。

参考 Array.prototype.every() - JavaScript | MDN

ここで生成したconst filteredDATAを、return内で表示させればいいわけですね。

絞り込みフィルターを通したリストを表示

ここまでで、const DATAconst filteredDATAとして整形しなおしました。

このfilteredDATAを、return内で展開・表示させます。

return(
  <>
    <ul>
      {filteredDATA.map((node) => (
        <li key={node.id}>{node.title}</li>
      ))}
    </ul>
  <>
)

この時点ではまだ絞り込み用のチェックボックスはなく、絞り込みはできません。

次に絞り込み用のチェックボックスを作ります。

絞り込み用のタグのチェックボックスを作成

絞り込みを行うため、タグのチェックボックスを作ります。

return (
  <>
    <div>
      <label htmlFor="english">
        <input
          type="checkbox"
          onChange={filterHandler}
          value="english"
          id="english"
        />
        <span>English</span>
      </label>
      <label htmlFor="french">
        <input
          type="checkbox"
          onChange={filterHandler}
          value="french"
          id="french"
        />
        <span>French</span>
      </label>
      <label htmlFor="kids">
        <input
          type="checkbox"
          onChange={filterHandler}
          value="kids"
          id="kids"
        />
        <span>Kids</span>
      </label>
      <label htmlFor="adults">
        <input
          type="checkbox"
          onChange={filterHandler}
          value="adults"
          id="adults"
        />
        <span>Adults</span>
      </label>
    </div>

    <div>
      <ul>{/* ... */}</ul>
    </div>
  </>
)

ここでもハードコーディングしていますが、実際の現場ではやはりタグをmap()で展開することになると思います。生成方法は適宜アレンジしてください。

それぞれのinputタグにおいて、チェックが入った時・外れた時に発火するハンドラー(ここではfilterHandlerという関数名で作成)を設定します。ハンドラーはすべて共通です。このハンドラーによって、filterTags(チェック済みタグのリスト)を逐次書き換えます。

ハンドラ自体は以下のようにしました。

const filterHandler = event => {
  if (event.target.checked) {
    setFilterTags([...filterTags, event.target.value])
  } else {
    setFilterTags(
      filterTags.filter(filterTag => filterTag !== event.target.value)
    )
  }
}

ユーザーによってチェックされたらfilterTags(チェック済みタグのリスト)に当該タグを追加。チェックが外れたらfilterTagsからそのタグを削除。至ってシンプルな内容です。

コードまとめ

すべてのコードをまとめると、以下の通りになります。

import { useState } from "react"

export default function Filter() {
  const DATA = [
    {
      id: 1,
      title: "Enjoy studying English",
      tags: [
        {
          id: "tag1",
          title: "English",
          slug: "english",
        },
        {
          id: "tag2",
          title: "For kids",
          slug: "kids",
        },
      ],
    },
    {
      id: 2,
      title: "Parlons français",
      tags: [
        {
          id: "tag3",
          title: "French",
          slug: "french",
        },
        { id: "tag2", title: "Kids", slug: "kids" },
      ],
    },
    {
      id: 3,
      title: "Intermediate English",
      tags: [
        {
          id: "tag1",
          title: "English",
          slug: "english",
        },
        {
          id: "tag4",
          title: "Adults",
          slug: "adults",
        },
      ],
    },
    {
      id: 4,
      title: "How to study French",
      tags: [
        {
          id: "tag3",
          title: "French",
          slug: "french",
        },
        {
          id: "tag4",
          title: "Adults",
          slug: "adults",
        },
      ],
    },
  ]

  const [filterTags, setFilterTags] = useState([])

  const filteredDATA = DATA.filter(node =>
    filterTags.length > 0
      ? filterTags.every(filterTag =>
          node.tags.map(tag => tag.slug).includes(filterTag)
        )
      : DATA
  )

  const filterHandler = event => {
    if (event.target.checked) {
      setFilterTags([...filterTags, event.target.value])
    } else {
      setFilterTags(
        filterTags.filter(filterTag => filterTag !== event.target.value)
      )
    }
  }

  return (
    <>
      <div>
        <label htmlFor="english">
          <input
            type="checkbox"
            onChange={filterHandler}
            value="english"
            id="english"
          />
          <span>English</span>
        </label>
        <label htmlFor="french">
          <input
            type="checkbox"
            onChange={filterHandler}
            value="french"
            id="french"
          />
          <span>French</span>
        </label>
        <label htmlFor="kids">
          <input
            type="checkbox"
            onChange={filterHandler}
            value="kids"
            id="kids"
          />
          <span>Kids</span>
        </label>
        <label htmlFor="adults">
          <input
            type="checkbox"
            onChange={filterHandler}
            value="adults"
            id="adults"
          />
          <span>Adults</span>
        </label>
      </div>
      <ul>
        {filteredDATA.map(node => (
          <li key={node.id}>{node.title}</li>
        ))}
      </ul>
    </>
  )
}

今回はソート(並び替え)を入れていませんが、DATAをソートして、そのソートしたデータを元にfilteredDATAを生成すれば、ソート(並び替え)をしながら絞り込みをすることも可能です。いずれ解説したいと思います。