Vuex でリストのアイテムごとに動的にストアを持つ

## tl;dr

今回のサンプルを以下で公開していますので御覧ください:
- https://mangano-ito.github.io/vuex-dynamic-module-impl/
- https://github.com/mangano-ito/vuex-dynamic-module-impl

## 要件

よくある複数のアイテムがあり、
各アイテムには「**ID** (`id`)」と「**いいね** (`like`)」を持つデータを扱いたいとします。

そして、個別のアイテムに対して **いいねを増やす** (`upvote`), **いいねを減らす** (`downvote`) できるような機能をもつとします。

画面のイメージとしてはこういう感じのリスト表示になりそうですね:

![vue-test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359439/688f761a-0b18-2ec3-dde3-e35303271bea.png)

この要件に対する Vuex の実装を考えてみましょう。

## データ構造

API 側ではこういうレスポンスでデータが取得できる感じだとします:

```json:list.json
[
  {
    "id": 0,
    "like": 50
  },
  {
    "id": 1,
    "like": 10
  },
  {
    "id": 2,
    "like": 100
  }
]
```

(もしかしたら `Array` でなく `Object` で渡してくれるかもしれませんが、なにかしらのコレクションが与えられるとします)。

こういったデータ構造に対してストアをもつ場合:

```ts:リストのストアのイメージ

interface Item {
    id: number;
    like: number;
}

interface State {
    // ID からの操作が多そうなので Object として管理します
    // key は各アイテムの ID になりそうですね
    items: {[key: number]: Item};
}

const initialState: State = {
    items: {},
};

const mutations: MutationTree<State> = {
    // いいねを増やすミューテーション
    upvote(state: State, payload: {id: number}) {
        const item = state.items[payload.id];
        if (item) {
            ++item.like;
        }
    }
};
```
(👉 [リストストアのサンプル実装 on GitHub](https://github.com/mangano-ito/vuex-dynamic-module-impl/blob/72e63a1cb023db9b672f51075044482d498a2193/src/stores/list/store.ts))

このように、ひとつのリストのストアを作成して、ステートとしてアイテムのコレクションを持つことになりそうです。

## 懸念

しかしながら、この持ち方だとリスト内のあるアイテムをいいねするためには、
`Array` を走査するなり、`Object` でキー・バリュー的に持っておいて取得するかたちになるので、
ケースによってはちょっと管理しづらいと思うときがあります。

## アイテムごとにストアを持ちたいとき

個別のアイテムごとにストアを持てば、
それぞれのアイテムにフォーカスしてステートを管理できてシンプルになりそうです。

```typescript:アイテムのストアのイメージ
interface State {
    id: number;
    like: number;
}

const initialState: State = {
    id: 0,
    like: 0,
};

const mutations: MutationTree<State> = {
    upvote(state: State) {
        ++state.like;
    }
};
```
(👉 [アイテムストアのサンプル実装 on GitHub](https://github.com/mangano-ito/vuex-dynamic-module-impl/blob/63722c5e47f4e94b8d3276706d43293c0acf6436/src/stores/item/store.ts))

ただ、通常の静的なストアではシングルトンのように共通のものになるため、それぞれ独立したストアを持つことはできません。

なので、動的にストアを登録するようなモジュールを作成します:

```ts:DynamicModule.ts
import { Vue } from 'vue/types/vue';
import { Module } from 'vuex';

export default class DynamicModule<T, R> {
    constructor(
        private instance: Vue,
        private namespace: string,
        private module: Module<T, R>,
    ) {
        this.attach();
    }

    public attach() {
        this.instance.$store.registerModule(this.namespace, this.module);
    }

    public detach() {
        this.instance.$store.unregisterModule(this.namespace);
    }

    public get state(): T {
        return this.instance.$store.state[this.namespace];
    }

    public action<ActionType>(action: ActionType, payload?: any): any {
        return this.instance.$store.dispatch(`${this.namespace}/${action}`, payload, {root: true});
    }

    public getter<GetterType>(getter: GetterType): any {
        return this.instance.$store.getters[`${this.namespace}/${getter}`];
    }
}
```
(👉 [動的モジュールヘルパーのサンプル実装 on GitHub](https://github.com/mangano-ito/vuex-dynamic-module-impl/blob/94251dc02581c5123a674a3c84d5b980781a6be7/src/helper/dynamic_module.ts))

`Component` では `created` で渡されたアイテムから動的にモジュールを登録するようにします:

```ts:ItemComponent.ts
@Component
export default class ItemComponent extends Vue {
    /** ID of item */
    @Prop() private id!: number;
    /** module of the item */
    private module!: DynamicModule<State, any>;

    /**
     * The number of likes given
     */
    public get likes(): number {
        return this.module.state.likes;
    }

    /**
     * Upvote item
     */
    public upvote() {
        return this.module.action(Actions.UPVOTE);
    }

    /**
     * Downvote item
     */
    public downvote() {
        return this.module.action(Actions.DOWNVOTE);
    }

    protected created() {
        const namespace = `items/${this.id}`;
        this.module = new DynamicModule(this, namespace, Store);
        this.module.action(Actions.INIT, this.id);
    }

    protected destroyed() {
        this.module.detach();
    }
}
```
(👉 [コンポーネントのサンプル実装 on GitHub](https://github.com/mangano-ito/vuex-dynamic-module-impl/blob/7e4af2dc76b9180e679df60acbce309feda1c48c/src/components/ItemImpl1.vue))

こうすることで、各アイテムごとのコンポーネントから各アイテムごとの独立したストアが登録されるようになり、アイテム単位でのストア管理が行えるようになります。

## サンプル

https://mangano-ito.github.io/vuex-dynamic-module-impl/

今回のサンプルを GitHub Pages 上に置いて実行できるようにしてみました。

`Implementation 1` は動的にアイテムごとにストアを持って管理するようにした実装で、
`Implementation 2` は静的にリスト全体でストアを持つようした実装です。
リンクでそれぞれの実装のリスト表示を切り替えることができます。

実行するとわかるように、動的である分 `1` の実装ではコンポーネントが生成/破棄されるたびにストアも生成/破棄されるようになってるので、切り替え時にステートが保存されていません。

対して、`2` は静的にストアを持つぶん、切り替えしてもステートが保持されています。

実装して思ったのは、`1` は動的にストアが増える分管理が煩雑になる場合もありそうだということで、どちらも良し悪しなので要件に応じて考えたほうがよいかと思います。