VuexをVue Composition APIで実装する！【NoVue】 

こんにちは。shundroidです。5年ぶりにQiita書いてます！
大学の授業が始まって早1週間。
最初はガイダンスばかりですが、だんだん大学の授業の面白さが分かってきました。

さて、今回は、[vue-composition-api](https://github.com/vuejs/composition-api)を使っていきたいと思います。
これが凄いんですよ。
おそらく、最初のモチベーションとしては、肥大化したViewModelを整理する、ということだったと思うんですが、
おなじみの `watch` や、リアクティブな変数を生成する `reactive` などをコンポーネントから分離して利用できるようになったことで、後で詳しく述べますが、**幅広く使える可能性を備えている**と思います。

特に、これはデータバインドを活用したアーキテクチャを設計し、プロトタイプを作るのにも向いていると考えたので、
ここでは、試しに**Vuexをvue-composition-apiを使って実装してみたいと思います！**

## Composition API の基本的な機能

詳しくは、Vue.jsの公式ドキュメントが分かりやすくて、APIが作られた背景までわかるようになっているので、そちらを見てください。

https://v3.ja.vuejs.org/guide/composition-api-introduction.html

今回特に使う機能を紹介していきます。

### ref

リアクティブな変数を生成します。似たものに `reactive` というのがありますが、こちらは値がオブジェクトの時に使えて、 `.value` を省略できます。

コードの説明はTypeScriptで行っていきます。

```typescript
import { ref, Ref } from '@vue/composition-api';
// (予めVueにComposition APIを登録する必要はある:後のstore.ts参照)

// どういう型になるか説明するために、型指定を行っておきます。
const num1: Ref<number> = ref(0);
const num2 = num1;

console.log(num1.value, num2.value); // 0, 0
num2.value++;
console.log(num1.value, num2.value); // 1, 1
```

これだと、ただ参照値を渡しているだけで、例えば、次のようにしても良いのではないかと思うかもしれません。

```typescript
const num1 = { value: 0 };
const num2 = num1;
```

ですが、 `ref` の強みは、**Vueの便利なリアクティブシステムに値するものがすべて使える**ところにあります。
`watch`、`computed`などです。これを次に説明します。

### watch

Vueでおなじみのものと同じです。

```typescript
import { ref, watch, Ref } from '@vue/composition-api';

const num = ref(0);

watch(num, () => {
  console.log('changed!');
});

num.value--; // changed!

```

こういった機構を簡単に設計できるのです。
**Vueのコンポーネントシステム以上の可能性**を感じますよね。

### おまけ：computed

今回は使いませんでしたが、 `computed` もシンプルに使えます。

```typescript
import { ref, computed, Ref } from '@vue/composition-api';

const num1 = ref(0);
const num2 = computed(() => num1.value + 1);

console.log(num1.value, num2.value); // 0, 1
num1.value++;
console.log(num1.value, num2.value); // 1, 2
```

すごいなあ。
こういうプログラムが普及すると、**プログラムは上から実行されるんだよという説明が難しくなってきそう**ですよね。
もちろん間違ってはいないですが、このようなcomputed内のコールバックが実行される順番等を踏まえると、もっと別の意味づけのほうが適切なのではないか、と思えてしまいます。

プログラミング言語の**命令型→宣言型への移行**が垣間見える気がします。
[個人的には、データフロープログラミングに興味がある](https://asterisks.netlify.app/2021/04/05/interest-2021-04/#%E3%83%87%E3%83%BC%E3%82%BF%E3%83%95%E3%83%AD%E3%83%BC%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0)ので、この流れは賛成です。

computedが内部的にどのようにコールバックの実行タイミングを見極めているか、については、下の記事が分かりやすかったです。一旦中身を実行してみて、どのリアクティブ変数が取得されたか、を記録しているようですね。

https://qiita.com/neutron63zf/items/506c7493a53cea44860e

条件分岐などがcomputed内であった場合は、一旦実行するのみでは足りないのではないか、と思いましたが、条件自体がリアクティブだとこれで足りそうですね。よくできてるなあ。

### VuexをNo Vueで実装していく

ここまでで Composition API の基本的な機能を見てきました。
Vueのコンポーネントに止まらず、**もっと汎用的な使い道がある**ということがわかりました。

というわけで、実際の応用例として、Vuex的アーキテクチャを実装してみましょう。

![](https://lh3.googleusercontent.com/pw/ACtC-3fvBg56YBtBKrcmPsK3eIcwgKh4L0vFEf3eD04sThgCZkRDxDnFeu6qG5mdgLZ53Nla300Bz0an7Ku-R1NpiBIlWdbSklSzQ0Roezf5yBlfwCuokUUbVctkKNGfgWEeh2Enj-7t-CJxf1XWss2ioRkrNg=w955-h639-no?authuser=0)

今回はTodoアプリを作っていきますが、
あまり本質的でない部分は端折ります。
**Actionはごめんなさい、さようなら。**
そして本当はAction呼び出しは疎結合に行われますが、これはあまりメリットがないので、今回はMutation直呼びで代用します。

### store.tsの実装

```typescript:/store.ts
import Vue from 'vue';
import VueCompositionAPI, { DeepReadonly, reactive, readonly, ref, Ref, UnwrapRef } from '@vue/composition-api';
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>;
Vue.use(VueCompositionAPI);

interface TodoItem {
  title: string;
  completed: boolean;
}

interface State {
  todoList: Ref<TodoItem[]>;
}

const state: State = {
  todoList: ref([])
};

// mutations
export function addTodo(title: string) {
  state.todoList.value.push({ title, completed: false })
}

const readonlyState: { [P in keyof State]: DeepReadonly<UnwrapNestedRefs<State[P]>> } = {
  todoList: readonly(state.todoList)
}
export default readonlyState;
```

Composition APIを使うために、仕方なくVueを呼び出していますが、
あくまでVueをアプリケーションで使うわけではないです。

stateの定義に `ref` を用いています。これは先ほど紹介した通りです。
本当は、Model部分はViewに影響されず、純粋なモデルにするべきですが、リアクティブ化する程度ならばいいかと思ってしまいました。どうですかね。
あと、 `reactive([])` では配列はリアクティブになりません。注意してください。

mutationは普通にstateを書き換えています。

そして、**ここが肝**なのですが、
stateをexportするときに、**stateをreadonlyにしています！！！！！**
実は、ライブラリのほうのVuexはstateに双方向データバインドがかかっていて、
**Mutationを介さずにバインドによってstateが書き換えられてしまう**のですが、
これはどう考えてもbadなので、今回は**Composition APIの機能をフル活用して**、
readonlyにしています。

readonlyStateの型ですが、Composition API内部で使われている型を拝借しました。
`[P in keyof Type]` というのは、Mapped TypesというTypeScriptの機能です。**反復処理の型バージョン**みたいなやつです。
DeepReadonlyでは、 `T extends U ? A : B` という、**型バージョンの条件分岐**を使っているようです（参考：API内部）。
どうしてここまでして型を組みたいかというと、こうすることで、readonlyStateの値に関して保証ができるからです。
つまり、stateにあるのにreadonlyStateにない！とか、readonlyになっていない！とかいうことが、**TypeScriptのエラーとして把握できます**。
このように、ただ型がつくと嬉しい！楽しい！というだけでなく、開発上のミスが無いようにするために型を使うべきです。この使い方は `never` 型などでよくします。

## 個々のコンポーネントの実装

まずはインターフェース実装。

```typescript:/components/component.ts
export default interface Component {
  render(): HTMLElement
}
```

Todoを追加するやつ。

```typescript:/components/todoList.ts
import Component from './component';
import state from '../store';
import { watch } from '@vue/composition-api';

export default class TodoList implements Component {
  render() {
    const ul = document.createElement('ul');
    watch(state.todoList, () => {
      ul.innerHTML = '';
      for (let todoItem of state.todoList.value) {
        const li = document.createElement('li');
        li.textContent = `${todoItem.title}`
        ul.appendChild(li)
      }
    });
    return ul;
  }
}
```

**VuexなのにtextContentとかdocumentとか出てきてて草**
今回は、**Composition APIの汎用性**を説明したかったので、
**バニラでも十分役立つんだ！**ということを説明したくてこうしています。

もちろん、バリバリVue使って実装してもらってもいいです。

ここでは、`state.todoList`を`watch`しています。
さすがに直DOMに流すのはリアクティブにはできないので仕方ない。
また、ご確認いただけると幸いですが、`state.todoList`はしっかりreadonlyになっているので、**Viewからは改変できません！わーい**
じゃあViewからstateを変えるにはどうするの？というのが次のコンポーネントになります。

```typescript:/components/addTodo.ts
import Component from './component';
import { addTodo } from '../store';

export default class AddTodo implements Component {
  render() {
    const parent = document.createElement('div');
    const input = document.createElement('input');
    input.type = 'text';
    parent.appendChild(input);
    const button = document.createElement('button');
    button.textContent = 'Add a task';
    button.addEventListener('click', () => {
      addTodo(input.value);
      input.value = '';
    });
    parent.appendChild(button);
    return parent;
  }
}
```

mutationをimportして使っています。

## エントリーポイント

ここはあまり大事ではないですが、コンポーネントを登録(物理)します。

```typescript:/main.ts
import AddTodo from './components/addTodo';
import TodoList from './components/todoList';

const app = document.querySelector('#app');
app.appendChild(new AddTodo().render());
app.appendChild(new TodoList().render());
```

Vueが完全に隠れていて気持ちがいいですね。

## 出来上がったものがこちらになります。

~~冷凍庫で3時間冷凍したものが~~Githubにあります。

https://github.com/shundroid/vuex-no-vue-sample/

実装には[Vite](https://vitejs.dev/)を用いました。
速いですね。今風でいいですね。webpackもそろそろ引退かな。

デモは[こちら](https://shundroid-vuex-no-vue.netlify.app/)で試すことができます。

## まとめ

いかがでしたでしょうか。Composition APIの汎用性が伝われば幸いです。
個人的には、アーキテクチャの実装等に興味があるので、
データバインドを活用したアーキテクチャのプロトタイプ作成などにどんどん使っていこうかと思います。

※この記事は[僕のブログ](https://asterisks.netlify.app/2021/04/13/vuex-no-vue/)を移植して書いたものです。