こちらはVue.js #4 Advent Calendar 2017 - Qiita 12日目の記事です。
株式会社SCOUTERの小平(id:ryotakodaira) です。
今回はVue.js用の状態管理パターン+ライブラリの「Vuex」を使って、一般的なサービスでよく見かける絞り込み機能を例に実際の実装方法を紹介していきます。
Vuexを導入することでアプリケーションの状態を集中的に管理でき、状態の変更を特定の場所からのみ許可することで予期しない状態変更が起きにくいなどのメリットがあると思います。
また、Vueコンポーネントを細分化していくとコンポーネント間の状態のやり取りにsync修飾子などを使って状態の受け渡しを行うようになるのですが、徐々に状態の受け渡しのコードが複雑になってしまいます。通常このような場合、状態管理パターンを導入して状態遷移を簡単にすることができます。
今回はサンプルとしてQiitaのAPIを使用して簡単に記事のページングや絞り込みを行うことのできるページを作成しました。
完成例
検証実施環境
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
- 記事をフィルタリングするためのフォームを実装
Store
itemという名前をモジュールを作成しています。
そこまでコード量が多くなかったため、1つのファイルに action
, getter
, mutaion
を記述しています。
(mutationなどの細かい実装はこちらを御覧ください)
作成したactionは3種類です。
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 ()
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>
まとめ
QiitaTimeLineコンポーネント
では queryParams
を watch
しているため、検索条件が変更されるたびにloadメソッドが呼ばれて、記事の一覧を自動で更新することができます。
そのおかげで、毎回のように明示的にloadメソッドを呼び出して一覧を取得し直す必要がなくなりました!検索条件を新しく追加したいときも queryParams
を更新するメソッドを作って適切に呼び出してやるだけでOKです。
この様に状態の変更をフックにすることで、いちいち検索ボタンをクリックしなくても簡単に検索条件を反映することができます。
尚、今回実装したサンプルの完全版はGitHubに公開していますので良かったら触ってみてください!