Vue 3 でライブラリを使わない データストア を作る

以前読んだ、Software Design 2020 年 9 月号 で Vue.js の特集がありました。
この企画の記事の中で、ライブラリを使わないデータストアについての、紹介があります。
小規模アプリなら十分使えそうなものだとその時感じていました。

このライブラリを使わないデータストア相当のものを、Vue 3 Composition API で書けないかと試したので、メモです。

要件

  • 外部のライブラリを使わない
  • なるべく簡単に使える
  • よくある共通の値を参照しているカウンタを作る

実装

準備

開発環境の準備は、viteで用意する。

1
2
3
4
5
6
npm init @vitejs/app app --template vue-ts

cd app

npm install
npm run dev

これで Vue.js のよく見る初期画面を確認できる。

データストアを作る

次のようにデータストアを作ってみました。
データストアを作るというよりも、データの作成と更新をコンポーネントから切り出した感じですね。

app/src/store/store.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { ref, computed, ComputedRef } from "vue";

export interface store {
count: ComputedRef<number>;
up: () => void;
down: () => void;
}

const _count = ref(0);

const up = () => {
_count.value++;
};

const down = () => {
_count.value--;
};

// 値はcomputedをかませてreadonlyに
const count = computed(() => _count.value);

export default { count, up, down };

自作データストアを使う

続けて、自作したデータストアを使ってみます。

app/src/components/counter.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div class="counter">
Count: {{ count }}
<button @click="up">count ++</button>
<button @click="down">count --</button>
</div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
// データストアのインポート
import store from "../store/store";

export default defineComponent({
setup: () => {
// データストアをから変数・関数取得
const { count, up, down } = store;
return { count, up, down };
},
});
</script>

<style scoped>
.counter {
margin: 10px;
}
</style>

この counter.vue の呼び出し側は次のようになります。

app/src/App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<div>
<Counter />
<Counter />
</div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import Counter from "./components/counter.vue";

export default defineComponent({
name: "App",
components: {
Counter,
},
});
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

動作確認

npm run dev で起動すると次のようになります。

カウンターのコンポーネント 2 つの間で値を共有して取り扱うことができました。

データストアを provide(2021-05-23 追記)

今回より簡単なただ 1 つの変数を provide する記事を前に書いていたことを、この記事公開してから気が付きました。

今回作ったデータストアも provide で公開して、inject で使用してみます。

ストアの修正

呼び出しに使用するキーを記述します。

app/src/store/store.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { ref, computed, ComputedRef, InjectionKey } from "vue";

export interface store {
count: ComputedRef<number>;
up: () => void;
down: () => void;
}

// キーの追加
export const storeKey: InjectionKey<store> = Symbol("store");

const _count = ref(0);

const up = () => {
_count.value++;
};
const down = () => {
_count.value--;
};

// 値はcomputedをかませてreadonlyに
const count = computed(() => _count.value);

export default { count, up, down };

provide で登録する

app/main.ts
1
2
3
4
5
6
7
8
import { createApp } from "vue";
import App from "./App.vue";
import store, { storeKey } from "./store/store";

const app = createApp(App);
// データをprovideで登録
app.provide(storeKey, store);
app.mount("#app");

inject で呼び出す

provide で登録したデータを、inject で呼び出してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div class="counter">
Count: {{ count }}
<button @click="up">count ++</button>
<button @click="down">count --</button>
</div>
</template>

<script lang="ts">
import { defineComponent, inject } from "vue";
import { store, storeKey } from "../store/store";

export default defineComponent({
setup: () => {
// injectの返す値の型を'store | undefined'と判断してしまいビルドに失敗
// アサーションを記述しておく。
const { count, up, down } = inject(storeKey) as store;
return { count, up, down };
},
});
</script>

<style scoped>
.counter {
margin: 10px;
}
</style>

画面を確認すると、先に作ったものと同様に動きます。


データ部分だけの切り出しを行ってみた今回。
リアクティブなデータで結果を共有し、API のレスポンスをキャッシュできるようにしたりすると、より面白そうです。

ではでは。