Closed9

[キャッチアップ] Pinia

shingo.sasakishingo.sasaki

概要

  • Pinia は Vuex 5 が目指すグローバルステートの扱いの実証実験の立ち位置
  • Vuex はコミュニティ内の RFC を通じて慎重に仕様が固められていくので、その哲学をテストとして開発される
  • 将来的な Vuex5 への移行や統合をしやすいように設計されている
  • Vue2, Vue3 両方に対応し、SSR, CSR いずれも可能
shingo.sasakishingo.sasaki

Vuex 3,4 と比べて

  • TypeScript の完全なサポート
  • CodeSpliting
  • Mutations の廃止 (多くの場合それは冗長なので)
  • ネストされたモジュールの廃止
  • ネームスペースの廃止
shingo.sasakishingo.sasaki

プロジェクト作る (Vite, Vue3, TypeScript)

$ yarn create @vitejs/app 1030_pinia --template vue-ts
$ cd 1030_pinia
$ yarn install

スキャフォルドされる Hello, World コードを適当に削除してまっさらなアプリケーションにする。
(この辺もテンプレートの設定でできる気がする)

shingo.sasakishingo.sasaki

pinia をインストール

$ yarn add pinia

今回はs最新の 2.0.0 を使用する

shingo.sasakishingo.sasaki

Vue App での pinia の使用を宣言する

src/main.ts
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";

createApp(App).use(createPinia()).mount("#app");
shingo.sasakishingo.sasaki

ストアを作成する

src/store/todos.ts
import { defineStore } from "pinia";

type FilterType = "all" | "finished" | "unfinished";
type TODO = {
  id: number;
  label: string;
  finished: boolean;
};

export const useTodoStore = defineStore("todos", {
  state: () => {
    return {
      filter: "all" as FilterType,
      todos: [] as TODO[],
      nextId: 0,
    };
  },
  getters: {
    findTodo(state) {
      return (id: number): TODO => {
        const todo = state.todos.find((todo) => todo.id === id);
        if (todo === undefined) throw new Error("todo not found");

        return todo;
      };
    },
    finishedTodos(state) {
      return state.todos.filter((todo) => todo.finished);
    },
    unfinishedTodos(state) {
      return state.todos.filter((todo) => !todo.finished);
    },
    filteredTodos(state): TODO[] {
      switch (state.filter) {
        case "finished":
          return this.finishedTodos;
        case "unfinished":
          return this.unfinishedTodos;
        default:
          return this.todos;
      }
    },
  },
  actions: {
    addTodo(label: string) {
      this.todos.push({ id: this.nextId++, label, finished: false });
    },
    toggleTodo(id: number) {
      const todo = this.findTodo(id);
      todo.finished = !todo.finished;
    },
  },
});

shingo.sasakishingo.sasaki

コンポーネント側

App.vue
<template>
  <input v-model="state.newTodoLabel" />
  <button @click="addTodo">add</button>

  <input id="all" type="radio" v-model="filter" value="all" />
  <label for="all">すべて</label>
  <input id="finished" type="radio" v-model="filter" value="finished" />
  <label for="finished">完了済み</label>
  <input id="unfinished" type="radio" v-model="filter" value="unfinished" />
  <label for="unfinished">未完了</label>

  <ul>
    <li
      :class="{ todo: true, finished: todo.finished }"
      :key="todo.label"
      v-for="todo in filteredTodos"
      v-text="todo.label"
      @click="toggleTodo(todo.id)"
    />
  </ul>
</template>

<script setup lang="ts">
import { storeToRefs } from "pinia";
import { reactive } from "vue";
import { useTodoStore } from "./store/todos";

const state = reactive({ newTodoLabel: "" });
const store = useTodoStore();
const { filteredTodos, filter } = storeToRefs(store);

const toggleTodo = (id: number) => store.toggleTodo(id);
const addTodo = () => {
  if (state.newTodoLabel !== "") {
    store.addTodo(state.newTodoLabel);
    state.newTodoLabel = "";
  }
};
</script>

<style scoped>
.todo {
  user-select: none;
  cursor: pointer;
}
.todo.finished {
  text-decoration: line-through;
  color: gray;
}
</style>
このスクラップは2021/12/09にクローズされました