Vueで項目が多い検索フォームでの状態の持ち方

2 min read読了の目安(約2400字

モチベーション

20 個以上の検索項目を含む画面を作成する機会があったのですが、その際に検索条件の状態をどう持つかについて迷ったのでそのメモとして書きます。

React の場合は規模感によって useContext や useReducer を使用したり、親コンポーネントに検索条件をまとめておいて Props でそれらを渡すなどの方法(もちろんこれは Vue でも可能ですが)が考えられますが、Vue の場合はどうすればいいかなと迷いました。

結論としては、かなり多い検索条件の場合は Composition API と project/inject を使用するようにしました。実際のデモはこちらです。コードはこちら

検索状態を持つcomposablesを作成する

composables に検索条件を保持する useSearch.ts を作成します。
今回は記事を検索すると仮定して、タイトルとカテゴリと作成者を検索条件として持たせます。(このくらいなら Props でどうにかできるので、本当はもっと多い検索条件の方がいいのですがデモとしてあまりに多いのも良くないので省略しました🙇‍♂️)

import { InjectionKey, reactive, ref, toRefs } from "vue";
import axios from "axios";

export const useSearch = () => {
  const result = ref<string[]>([]);
  const searchCondition = reactive<{
    title: string;
    category: string[];
    author: string;
  }>({
    title: "",
    category: [],
    author: "",
  });

  const search = async () => {
    const params = {
      title: searchCondition.title,
      category: searchCondition.category,
      author: searchCondition.author,
    };

    const res = await axios.get<string[]>("/posts", { params });
    result.value = res.data;
  };

  return {
    ...toRefs(searchCondition),
    result,
    search,
  };
};

export type SearchContext = ReturnType<typeof useSearch>;

export const SearchKey: InjectionKey<SearchContext> = Symbol("SearchContext");

searchConditionというオブジェクトに検索条件が入っており、searchメソッドでAPIを叩くという流れになっています。

Component

試しにタイトルの情報を操作するコンポーネントであるTitle.vueを見てみましょう。

<template>
  <label class="text-sm font-bold mb-2">タイトル</label>
  <input
    type="text"
    class="
      shadow
      appearance-none
      border
      rounded
      w-full
      py-2
      px-3
      leading-tight
      focus:outline-none
      focus:shadow-outline
    "
    v-model="title"
  />
</template>

<script lang="ts">
import { SearchContext, SearchKey } from "@/composables/useSearch";
import { defineComponent, inject } from "vue";

export default defineComponent({
  setup() {
    const { title } = inject(SearchKey) as SearchContext;
    return { title };
  },
});
</script>

<style scoped></style>

このようにそれぞれのフォームで inject した検索条件を渡すことで検索条件が増えても個々のコンポーネントで検索条件の操作を完結することができます。

まとめ

今回のデモのコードは階層も同じであり3つしか検索条件がないため Props で状態を共有してもいいですが、階層が深くなった場合などにこのパターンを使用しても良さそうです。