Vuexを使って絞り込み機能を実装してみた

こちらはVue.js #4 Advent Calendar 2017 - Qiita 12日目の記事です。

株式会社SCOUTERの小平(id:ryotakodaira) です。

今回はVue.js用の状態管理パターン+ライブラリの「Vuex」を使って、一般的なサービスでよく見かける絞り込み機能を例に実際の実装方法を紹介していきます。

Vuexを導入することでアプリケーションの状態を集中的に管理でき、状態の変更を特定の場所からのみ許可することで予期しない状態変更が起きにくいなどのメリットがあると思います。

また、Vueコンポーネントを細分化していくとコンポーネント間の状態のやり取りにsync修飾子などを使って状態の受け渡しを行うようになるのですが、徐々に状態の受け渡しのコードが複雑になってしまいます。通常このような場合、状態管理パターンを導入して状態遷移を簡単にすることができます。

今回はサンプルとしてQiitaのAPIを使用して簡単に記事のページングや絞り込みを行うことのできるページを作成しました。

完成例

f:id:ryotakodaira:20171211234935g:plain

 

検証実施環境

  • vue: ^2.5.2

  • vuex: ^3.0.0

  • QiitaAPI v2

初期設定

今回はvue-cliを使ってサクッとプロジェクトを立ち上げました。(vue-cliのインストールはこちらを参照)

vue init webpack qiita-time-line

追加で入れたパッケージは以下です。

{
    "axios": "^0.16.1",
    "element-ui": "^2.0.7",
    "lodash-es": "^4.17.4",
    "moment": "^2.19.4",
    "vuex": "^3.0.0"
}

package.json の変更が完了したら、 yarn install を実行しましょう。

コンポーネント

コンポーネントは以下の3つに分割しました。

  • QiitaTimeLine.vue

  • PageSearchOption.vue

    • ページを移動するためのフォームを実装
  • FilterSearchOption.vue

    • 記事をフィルタリングするためのフォームを実装

f:id:ryotakodaira:20171211234239p:plain

Store

itemという名前をモジュールを作成しています。 そこまでコード量が多くなかったため、1つのファイルに action, getter, mutaion を記述しています。 (mutationなどの細かい実装はこちらを御覧ください)

作成したactionは3種類です。

  • setQueryParams

    • APIに送信する検索条件を更新する
  • execGetItems

    • 検索条件と共にAPIにリクエストを送信する
  • setItems

    • 返ってきたデータをstateに格納する
import {cloneDeep} from 'lodash-es'

import types from '../mutation-types'
import initialState from '../initialState/itemList'
import qiitaApi from '../../api/qiitaApi'

const namespaced = true

const state = cloneDeep(initialState)

const getters = {
  state: state => state,
  queryParams: state => state.queryParams
}

const actions = {
  setQueryParams ({commit, state}, queryParams) {
    commit(types.SET_ITEM_LIST_SORT_DATA, {queryParams})
  },

  execGetItems ({commit, state}, queryParams) {
    commit(types.SEND_ITEM_LIST_REQUEST)

    return qiitaApi.getItems(queryParams)
  },

  setItems ({commit, state}, payload) {
    commit(types.RECEIVE_ITEM_LIST_RESPONSE, payload)
  }
}

const mutations = {
  [types.SET_ITEM_LIST_SORT_DATA] (state, {queryParams}) {
    Object.assign(state.queryParams, queryParams)
  },

  [types.SEND_ITEM_LIST_REQUEST] (state) {
    state.isLoading = true
  },

  [types.RECEIVE_ITEM_LIST_RESPONSE] (state, payload) {
    state.isLoading = false
    Object.assign(state, {items: payload})
  }
}

export default {
  namespaced,
  state,
  getters,
  actions,
  mutations
}

検索フォームの実装

まずはJS部分。 mapActions, mapGetters を使って、先程、storeで定義したactions, gettersを読み込みます。

次にフィルタリングを条件の変更を検知するためのwatcherを定義します。 ここでは、 computed で定義されている this.queryParams を監視の対象にしています。

この時に deep: true をつけるのを忘れないようにしましょう。これによりネストしたオブジェクトの中身の変更まで検知してくれるため、 this.queryParams の中身を一つずつwatchする必要がなくなります。 (watcherはこちらを参照)

後はAPIにリクエストを送信するためのメソッドと検索条件を更新するためのメソッドを用意します。

  • load ()

    • APIにリクエストを送信して記事の一覧を更新する
  • handleSelectFilterCondition ()

    • FilterSearchOptionコンポーネントに渡してフォームが変更されるたびに状態を更新する
  • handlePageChange ()

    • PageSearchOptionコンポーネントに渡してフォームが変更されるたびに状態を更新する
import {mapGetters, mapActions} from 'vuex'
import PageSearchOption from './PageSearchOption'
import FilterSearchOption from './FilterSearchOption'

export default {
  name: 'QiitaTimeLine',

  components: {
    PageSearchOption,
    FilterSearchOption
  },

  data () {
    return {}
  },

  mounted () {
    this.load()
  },

  computed: {
    ...mapGetters({
      state: 'item/state',
      queryParams: 'item/queryParams'
    }),
  },

  watch: {
    queryParams: {
      handler: function () {
        this.load()
      },
      deep: true
    }
  },

  methods: {
    ...mapActions({
      execGetItems: 'item/execGetItems',
      setQueryParams: 'item/setQueryParams',
      setItems: 'item/setItems'
    }),

    load () {
      this.execGetItems(this.queryParams).then(res => {
        this.setItems(res.data)
      }).catch(err => {
        console.log(err)
      })
    },

    handleSelectFilterCondition (val) {
      this.setQueryParams({query: val})
    },

    handlePageChange (num) {
      this.setQueryParams({page: num})
    }

  },
}

template部分も実装していきます。 PageSearchOption, FilterSearchOption コンポネントに検索条件を更新するための action(methodsで定義されているhandleSelectFilterCondition, handlePageChange)と現在選択している項目を選択済みにするために queryParams から該当する値を active-item-key として渡してあげます。

PageSearchOption, FilterSearchOption コンポーネントの詳細な実装はこちらを御覧ください!

<template>
  <div>
    <h2>Qiita Time Line</h2>
    <div class="filter">
      <PageSearchOption
        :active-item-key="this.queryParams.page"
        :action="handlePageChange"
      />
      <FilterSearchOption
        :active-item-key="this.queryParams.query"
        :action="handleSelectFilterCondition"
      />
    </div>
    <ul>
      <template v-if="this.state.isLoading === false">
        <li v-for="(item, key) in this.state.items">
          <a :href="item.url" target="_blank">
            <p class="trim green" style="font-weight: bold">{{item.title}}</p>
            <p>公開日時:{{item.created_at | formatJaTime }}</p>
            <p>いいね:{{item.likes_count}}</p>
            <p>タグ:<span class="tag" v-for="(tag, key) in item.tags">{{tag.name}}</span></p>
          </a>
        </li>
      </template>
      <li v-else>
        <p><i class="el-icon-loading"></i> Loading...</p>
      </li>
    </ul>
  </div>
</template>

まとめ

f:id:ryotakodaira:20171211234935g:plain

QiitaTimeLineコンポーネント では queryParamswatch しているため、検索条件が変更されるたびにloadメソッドが呼ばれて、記事の一覧を自動で更新することができます。 そのおかげで、毎回のように明示的にloadメソッドを呼び出して一覧を取得し直す必要がなくなりました!検索条件を新しく追加したいときも queryParams を更新するメソッドを作って適切に呼び出してやるだけでOKです。 この様に状態の変更をフックにすることで、いちいち検索ボタンをクリックしなくても簡単に検索条件を反映することができます。

尚、今回実装したサンプルの完全版はGitHubに公開していますので良かったら触ってみてください!

github.com