はじめに
最近流行っててみんな大好きなTypeScriptですが、VueやReactと合わせて開発するケースが増えてきています。
だがしかし、TypeScriptとVuexの相性は悪く、vuexのstate, action, mutation, getterをコンポーネント内で使おうとしたときに型安全が守られない、インテリセンスが効かない等の状態になり、非常に使い辛い。
これらの解決策としてサードパーティ製のパッケージであるvuex-module-decoratorsを使って優勝する方法をサンプルアプリケーションを元に記述していきます。
構成
今回はこちらのアプリケーションを元に書いていきます。
storeを分割して登録する
createStoreで使いたいmoduleを登録します。
これで用途毎にVuexをmoduleで分割することが出来ます。(いい話
import Vuex from 'vuex'
import Vue from 'vue'
import { UserModuleClass, registerUserModule } from './UserModule'
import { PhotoModuleClass, registerPhotoModule } from './PhotoModule'
Vue.use(Vuex)
export interface RootState {
UserModuleStore?: UserModuleClass,
PhotoModuleStore?: PhotoModuleClass,
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export default function createStore() {
const store = new Vuex.Store<RootState>({})
// とりあえず必要なmoduleはここでregisterする
registerUserModule(store)
registerPhotoModule(store)
return store
vuex-module-decoratorsを使って優勝する
画面からapiにリクエストを送る簡単なログイン認証とその状態管理を実装していきます。
store.tsの中身はこんな感じです。
vuex-module-decoratorsを使った場合ではstate, action, mutationの型を使うことが出来てます。
export default interface UserStateType {
userId: string;
name: string;
}
stateの型はsrc/modules/User.ts
にあるUserStateTypeで型を定義しています。
import { Mutation, Action, VuexModule, getModule, Module } from 'vuex-module-decorators'
import { Store } from 'vuex'
import { RootState } from './store'
import UserStateType from '../modules/User'
import ApiRequest, {SendCredential} from '../client/api'
@Module({name: 'UserModuleStore', namespaced: true, stateFactory: true})
export class UserModuleClass extends VuexModule {
user: UserStateType = {
userId: '',
name: '',
}
isLogin: boolean = false
@Mutation
public SET_USER(param: UserStateType) {
this.user = param
}
@Mutation
public SET_IS_LOGIN(param: boolean) {
this.isLogin = param
}
@Action
public async loginAction(credential: SendCredential) {
const result = await ApiRequest.postLoginRequest(credential)
if (result.data !== undefined) {
document.cookie = 'access_token=' + result.data.access_token + ';'
this.SET_USER({
userId: result.data.user_id,
name: result.data.name,
})
}
}
@Action
public async logoutAction() {
let accessToken = ''
const cookies = document.cookie
const cookiesArray = cookies.split('; ')
for (const c of cookiesArray) {
const keyValue = c.split('=')
if ( keyValue[0] === 'access_token') {
accessToken = keyValue[1]
}
}
await ApiRequest.postLogoutAction(accessToken)
}
@Action
public async isLoginCheckAction() {
let accessToken = ''
const cookies = document.cookie
const cookiesArray = cookies.split('; ')
for (const c of cookiesArray) {
const keyValue = c.split('=')
if ( keyValue[0] === 'access_token') {
accessToken = keyValue[1]
}
}
await ApiRequest.bearerAuthentication(accessToken).then((result) => {
this.SET_USER({
userId: result.data.user_id,
name: result.data.name
})
this.SET_IS_LOGIN(true)
}).catch(() => {
throw false
})
}
}
const UserVuexModule = (store?: Store<RootState>) => getModule(UserModuleClass, store)
export default UserVuexModule
export function registerUserModule(store: Store<RootState>) {
if (!store.state.UserModuleStore) {
store.registerModule('UserModuleStore', UserModuleClass)
}
}
storeはvuexのstoreを示しており、このモジュールがどのstoreに属しているのかを@Module
で明示的に示す必要あります。
moduleがclass化されたので、それぞれのメソッドの中でthisを使ってお互いを呼び出すことができる。
特に優れている点がactionsでのmutationの呼び出しです。
従来のvuexのactions内でのmutationやactionsの呼び方では
commit('SET_USER', 'any') //何でも受け付ける
のような書き方になり、型チェックが出来なかったのが
this.SET_USER({
userId: result.data.user_id,
name: result.data.name
}) // UserStateType型でないとエラーになる
のように記述できるようになる。
mutation SET_USER
は、引数にUserStateType型の値を要求しており、コンポーネントやactionsで呼び出したときも、vuexでの型チェックがしっかり行われている事がわかります。
コンポーネントからの呼び出し方
使用したいvuexのモジュールクラスをimportする
<template>
<div class="auth-nav">
<b-navbar type="dark" variant="info" toggleable="lg">
<b-button v-b-toggle.sidebar-backdrop variant="info">
<b-icon icon="list"></b-icon>
</b-button>
<b-navbar-brand href="/">
Vue.jsサンプル
</b-navbar-brand>
<Sidebar />
<b-navbar-toggle target="guest-navigation"/>
<b-collapse is-nav id="guest-navigation">
<b-navbar-nav class="ml-auto">
<b-nav-item to="/photos/upload/" right>写真を共有</b-nav-item>
<b-nav-item-dropdown right>
<template v-slot:button-content>
<em>{{ userId }}</em>
</template>
<b-dropdown-item-button @click="handleLogout">
LOGOUT
</b-dropdown-item-button>
</b-nav-item-dropdown>
</b-navbar-nav>
</b-collapse>
</b-navbar>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import Sidebar from '../sidevar/sidevar.vue'
import UserVuexModule from '../../../store/UserModule' // モジュールクラスをimport
export default Vue.extend({
name: 'AuthNav',
data() {
return {
userId: ''
}
},
components: {
Sidebar
},
async created() {
this.userId = UserVuexModule(this.$store).user.userId // stateの取得
},
methods: {
async handleLogout() {
await UserVuexModule(this.$store).logoutAction() // Actionの実行
window.location.href = '/'
}
}
})
</script>
<style scoped>
</style>
最後に
Vuex + TypeScriptはまだまだ発展途上で型安全性とインテリセンスに困らされるケースが多いと思います。
今回はvuex-module-decoratorsを使ってこの問題を解決してみましたが、他の解決法があれば調査してみたい気持ちです。