サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

Vue.js + Vuex入門 01: Storeを使う


VuexはVue.jsアプリケーションのための状態管理パターン+ライブラリです」。アプリケーションにひとつしかないStoreがデータなどの状態を集中してもち、状態は決まったやり方でしか変えられません。複数のコンポーネントから参照しても状態はつねに一意です。また、状態を変えるにも、いわば一本道をとおるしかないので、いつどのような変更が行われたのか、たやすくつきとめられます。

とはいえ、概念的な話だけでは、なかなかピンときません。Vuexが採り入れられた単一ファイルコンポーネントのVueアプリケーションをひとつ、チュートリアル形式でつくってみましょう。

01 題材はTodoMVC

お題は、Vue.js公式サイトの「TodoMVC の例」です。もっとも、このサンプルそのものは、単一ファイルコンポーネントのアプリケーションではありません。「Vue.js + CLI入門」シリーズで、単一ファイルコンポーネントにつくり直しました。その初回「Vue.js + CLI入門 01: リスト項目の表示と追加」でできたアプリケーションを出発点とします(サンプル001)。Vue.jsの構文やその解説については、この「Vue.js + CLI入門」シリーズをお読みください。本シリーズでは、Vuexの仕組みと構文を説明してゆきます。

サンプル001■vue-todo-mvc-01

ただし、前掲サンプル001には、「Vue.js + CLI入門 02」で採り上げた「日本語変換を確定する[enter]キーイベントの問題」があります。そこで、この項で改めたつぎの修正を加えてください。テキストフィールドに項目を入力したうえ、[return]/[Enter]キーでTodoリストに加わります(図001)。

src/components/TodoInput.vue: <template>

<input

	@keypress.enter="addTodo">
	<!-- @keydown.enter="addTodo"> -->

図001■テキスト入力フィールドから項目がリストに加えられる

図001

02 Vuexのインストール

まず、Vuexをインストールしなければなりません。詳しくは公式サイトの「インストール」のページをご覧ください。今回は単一ファイルコンポーネントのアプリケーションですので、「npm」の項にあるとおり、アプリケーションのディレクトリでコマンドラインツールから、つぎのように入力します。


npm install vuex --save

本稿執筆時にインストールされたのは、Vuex v3.1.1です。

package.json

{

  "dependencies": {

    "vue": "^2.6.10",
    "vuex": "^3.1.1"
  },

}

03 Storeをつくる

Vuexでまずつくるのは、状態を保管するStoreです。基本となる構文はつぎのとおりで、モジュールsrc/store.jsに定めることにします。Vue.use()メソッドでVuexプラグインをインストールしたら、Vuex.Storeコンストラクタでインスタンスを生成してください。コンストラクタに渡す引数のオブジェクトには、さまざまなオプションが与えられます。今回用いるのは、stategettersおよびmutationsの3つです。

src/store.js

import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
	state: {

	},
	getters: {

	},
	mutations: {

	}
});

前掲サンプル001は、標準的なVueアプリケーションとして、ルートモジュールのsrc/App.vueが状態を管理していました。それらを次項以降でそっくりStoreに移すことになります。VueとStoreのインスタンスオプションの対応は、つぎのとおりです。

 

データ

参照

変更

Vue

data

computed

methods

 

Vuex

state

getters

mutations

ルートコンポーネントsrc/App.vueには、src/store.jsモジュールをimportして、つぎのようにstoreオプションでインスタンスを注入してください。これで、すべての子コンポーネントがStoreを参照できるようになるのです。


import store from './store';

export default {

  store,

};

04 Storeにstateとgettersのオプションを定める

ルートコンポーネントsrc/App.vueから、src/store.jsモジュールにオプションの定めを移しましょう。オプションdataは、そのままstateに定められます。ただし、Storeはアプリケーションに唯一と決まっているため、returnで返しません。gettersのメソッドは、引数にstateを受け取りますので、そこから参照を取り出してください。値を返すことがはっきりし、記述も短くなるため、アロー関数=>が使われるようです。

src/App.vue

export default {

	/* data() {
		return {
			todos: [],
			uid: 0
		}
	},
	computed: {
		filteredTodos() {
			return this.todos;
		}
	}, */

}

src/store.js

export default new Vuex.Store({
	state: {
		todos: [],
		uid: 0
	},
	getters: {
		filteredTodos: (state) => state.todos
	},

});

ルートモジュールのsrc/App.vueから、子コンポーネントへのデータバインディング(:ディレクティブ)は外してください。

src/App.vue

<template>
	<section id="app" class="todoapp">

		<todo-list />
			<!-- :todos="todos"
			:filtered-todos="filteredTodos" -->
	</section>
</template>

子コンポーネントsrc/components/TodoList.vueは、データを親コンポーネントからpropsに受け取るのでなく、computedでStoreから直に参照できます。そのために用いるのが、$storeプロパティです。前述のとおりルートコンポーネントのインスタンスにstoreオプションを与えたことにより、このプロパティからStoreが参照できるようになりました。

src/components/TodoList.vue

export default {

	/* props: {
		todos: Array,
		filteredTodos: Array
	} */
	computed: {
		todos() {
			return this.$store.state.todos;
		},
		filteredTodos() {
			return this.$store.getters.filteredTodos;
		}
	}
}

05 Storeにmutationsオプションを定める

ルートコンポーネントsrc/App.vueのオプションmethodsに定めたメソッドは、src/store.jsモジュールのmutationsに移します。基本の処理は変わりません。ただし、第1引数にはstateを受け取るので、データはこの参照から取り出してください。第2引数は、メソッドを呼び出すコンポーネントから渡される値です。

src/App.vue

export default {

	/* methods: {
		addTodo(todoTitle) {
			const newTodo = todoTitle && todoTitle.trim();
			if (!newTodo) {
				return;
			}
			this.todos.push({
				id: this.uid++,
				title: newTodo,
				completed: false
			});
		}
	} */
}

src/store.js

export default new Vuex.Store({

	mutations: {
		// addTodo(todoTitle) {
		addTodo(state, todoTitle) {
			const newTodo = todoTitle && todoTitle.trim();
			if (!newTodo) {
				return;
			}
			// this.todos.push({
			state.todos.push({
				// id: this.uid++,
				id: state.uid++,
				title: newTodo,
				completed: false
			});
		}
	}
});

ルートモジュールのsrc/App.vueは、子コンポーネントからメソッドを呼び出すイベント(@ディレクティブ)の受け取りがなくなります。

src/App.vue

<template>
	<section id="app" class="todoapp">
		<header class="header">

			<todo-input

			/>
				<!-- @add-todo="addTodo" -->
		</header>

	</section>
</template>

コンポーネントからStoreのmutationsを呼び出すのが、commit()メソッドです。第1引数にはメソッド名を文字列で渡します。$emit()と異なり、属性のかたちで定めるディレクティブ(@)を介しませんので、ケバブケース(add-todo)でなくメソッド名のままのキャメルケース('addTodo')です(「ファイル名やスタイル名などで複数語を連結する方法(キャメルケース、スネークケース、ケバブケース)」参照)。

src/components/TodoInput.vue

export default {

	methods: {
		addTodo() {
			// this.$emit('add-todo', this.newTodo);
			this.$store.commit('addTodo', this.newTodo);

		}
	}
}

これで、前掲サンプル001のアプリケーションに、VuexのStoreが組み込めました。手を加えた4つのモジュールのの中身は、つぎのコード001に掲げます。とくに、ルートモジュールsrc/App.vueのJavaScriptコードは、すっきりと軽くなったことが見てとれるでしょう。実際にコードを確かめて試したい方は、CodeSandboxに公開した以下のサンプル002をご覧ください。

コード001■VuexのStoreを使う

src/store.js

import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
	state: {
		todos: [],
		uid: 0
	},
	getters: {
		filteredTodos: (state) => state.todos
	},
	mutations: {
		addTodo(state, todoTitle) {
			const newTodo = todoTitle && todoTitle.trim();
			if (!newTodo) {
				return;
			}
			state.todos.push({
				id: state.uid++,
				title: newTodo,
				completed: false
			});
		}
	}
});

src/App.vue

<template>
	<section id="app" class="todoapp">
		<header class="header">
			<h1>todos</h1>
			<todo-input />
		</header>
		<todo-list />
	</section>
</template>

<script>
import store from './store';
import TodoInput from './components/TodoInput.vue';
import TodoList from './components/TodoList.vue';
export default {
	name: 'app',
	store,
	components: {
		TodoInput,
		TodoList
	}
};
</script>
<style>
@import url("https://unpkg.com/todomvc-app-css@2.2.0/index.css");
</style>

src/components/TodoList.vue

<template>
	<section class="main" v-show="todos.length" v-cloak>
		<input class="toggle-all" type="checkbox">
		<ul class="todo-list">
			<li
				v-for="todo in filteredTodos"
				class="todo"
				:key="todo.id"
			>
				<todo-item
					:todo="todo" />
			</li>
		</ul>
	</section>
</template>

<script>
import TodoItem from './TodoItem.vue';
export default {
	name: 'TodoList',
	components: {
		TodoItem
	},
	computed: {
		todos() {
			return this.$store.state.todos;
		},
		filteredTodos() {
			return this.$store.getters.filteredTodos;
		}
	}
};
</script>
<style scoped>
[v-cloak] {
	display: none;
}
</style>

src/components/TodoInput.vue

<template>
	<input
		v-model="newTodo"
		class="new-todo" autofocus autocomplete="off"
		placeholder="What needs to be done?"
		@keypress.enter="addTodo"
	>
</template>

<script>
export default {
	name: 'TodoInput',
	data() {
		return {
			newTodo: ''
		};
	},
	methods: {
		addTodo() {
			this.$store.commit('addTodo', this.newTodo);
			this.newTodo = '';
		}
	}
};
</script>

サンプル002■vue-vuex-todo-mvc-01

Vue.js + Vuex入門


作成者: 野中文雄
作成日: 2019年09月19日


Copyright © 2001-2019 Fumio Nonaka.  All rights reserved.