Vueを再度学び直そうとした際に公式状態管理ライブラリがVuexではなくpinia(ピーニャ)に変更されていました。
色々調べて見ると2021年12月にはVueの公式プロジェクトとなったのがpiniaでした。(約2年後に知るという失態...)
ということで!学んだことをまとめて、簡単なTodoアプリを作成していきたいです。
piniaとは
piniaはVuexの状態管理ライブラリの後継になります。
公式プロジェクトなる以前からVue3のCompositionAPIベースで開発はされていたのが、前述した通り2021年12月に公式プロジェクトになりました。
公式プロジェクトになったためcreate-vue
を叩いた時にインストール・初期設定ができます。
また、既存のVueプロジェクトにはnpm install pinia
で追加可能です。(追加時は不要になるVuexをuninstallしてください)
Vuexとの違い
調べた感じ、多くの違いがあったが自分が気になったのは「TypeScriptのフルサポート」と「mutationsの廃止」、「Composition APIベースで書ける」です。
TypeScriptのフルサポート
Vuex4以前まではTypeScriptのサポートが不完全で使い勝手が悪い印象がありました。
ですが、pinia(Vuex5含め)では完全なサポートがされたため型推論されやすく使いやすいと感じました。
mutationの廃止
今までのVuexではstateの値更新をしたい場合はmutaionで行っていたが、piniaではmutaionが廃止されました。
mutationがあるのはデータ更新を限定することでactionsではAPIなどの非同期処理、mutaionではactionから受け取った値の加工・stateの更新という形で「責務の分離」が出来て自分的には好きな仕組みでした。
ただ、Vuexは学習コストが高いのは否めないです。(自分もVuexの流れを理解するまで約2ヶ月かかりました...)
学習コストが高いためVuexが理解しにくいというエンジニアは多くないはずです。
だからこそ、学習コストが下がったためにVueで状態管理が出来るエンジニアが増えてくれたらと思います。
Composition APIベースで書ける
後述しますが、piniaでは二種類の書き方があり、その中でComposition APIベースで書けるSetup Stores
はVue3以降のプロジェクトではComposition APIで書くためコードの統一化が出来るためチーム開発時やVue2以前のOption API
の書き方を知らないエンジニアにも書きやすくなっており学習コストが下がったと感じています。
piniaの書き方
piniaにはOption Stores
とSetup Stores
、二種類の書き方があります。
Option Stores
Vue2まで使われていたOption APIベースで書く方法になります。
現在のVue開発ではComposition API
の方がメジャーになっているためOption API
よりもSetup Stores
でpiniaを書くのが良いかと思います。
import { defineStore } from 'pinia'
type State = {
count: number
name: string
}
export const useCounterStore = defineStore('counter', {
state: (): State => ({ count: 0, name: 'Eduardo' }),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
}
}
})
Setup Stores
Vue3から導入されたComposition APIベースで書く方法になります。
現在は多くのエンジニアがComposition APIでVueを書いていると思うのでSetup Stores
でpiniaを書く方がチーム開発時などでコードの統一が出来て開発がしやすくなるかと思います。
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
チームによってはSetup Stores
ではなくOption Stores
を使うかと思います。
piniaを使った簡単なTodoアプリ作成
既にpiniaの使い方を知っている方は読み飛ばして問題ないです。
簡単なTodoアプリを作成して理解を深める内容になっています。
Vueのプロジェクトを作成
下記コマンドでプロジェクトを作成
npm create vue@latest
コマンドを叩いた後、複数の質問がされるので個々の作業しやすいプロジェクトにしてください。
自分は下記の形でプロジェクトを作成しています。
? Project name: › pinia-study-to-todo-app
? Add TypeScript? › Yes
? Add JSX Support? › No
? Add Vue Router for Single Page Application development? › Yes
? Add Pinia for state management? › Yes
? Add Vitest for Unit Testing? › No
? Add an End-to-End Testing Solution? › No
? Add ESLint for code quality? › Yes
? Add Prettier for code formatting? › Yes
プロジェクトが作成されたら
cd pinia-study-to-todo-app
npm install
を叩いて必要なライブラリをインストールしていきます。
viteの設定
自分が使いやすいように初期設定から少し変更を加えるだけのため不要な方は飛ばして問題ないです。
vite.config.tsにサーバーオプションを追加
viteの初期設定だとportが指定されていないので指定・開発用サーバー起動時にプロジェクトが自動でブラウザで開かれるようにしていきます。
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dns from 'dns' // 追加
dns.setDefaultResultOrder('verbatim') // 追加
// https://vitejs.dev/config/
export default defineConfig({
...,
// 追加
server: {
port: 3000,
open: true
}
})
Todoアプリ作成
ファイル構成
ファイル構成は下記になります。
基本的な構成はプロジェクト作成初期のままになっています。
.
├── Makefile
├── README.md
├── env.d.ts
├── index.html
├── package-lock.json
├── package.json
├── public
│ └── favicon.ico
├── src
│ ├── App.vue
│ ├── assets
│ │ ├── base.scss
│ │ ├── logo.svg
│ │ └── main.scss
│ ├── components
│ │ ├── HelloWorld.vue
│ │ └── TaskItem.vue
│ ├── main.ts
│ ├── router
│ │ └── index.ts
│ ├── stores
│ │ └── todo
│ │ ├── index.ts
│ │ └── type.ts
│ └── views
│ ├── CompletedTodosView.vue
│ ├── HomeView.vue
│ └── IncompleteTodosView.vue
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
storesフォルダ
今回メインになるフォルダになります。
状態管理したいものをフォルドごとで管理する形にしています。
stores
└── todo
├── index.ts
└── type.ts
-
index.ts
ではpiniaのコード -
type.ts
では状態管理している値の型を管理
export type TodoType = {
id: number
text: string
isCompleted: boolean
}
書き方としては一般的なComposition API
の書き方になっています。
Composition API
と同じ書き方のため、すぐに状態管理出来るのはPiniaの強みかと思います。
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import type { TodoType } from './type'
export const useTodoStore = defineStore('todo', () => {
const todos = ref<TodoType[]>([])
const idCount = ref(1)
const addTodo = (text: string) => {
todos.value.push({
id: idCount.value++,
text: text,
isCompleted: false
})
}
const todosFilteredCompleted = computed(() => {
return todos.value.filter((todo) => {
return todo.isCompleted
})
})
const todosFilteredIncomplete = computed(() => {
return todos.value.filter((todo) => {
return !todo.isCompleted
})
})
const searchTodoById = (id: number) => {
const todo = todos.value.find((todo) => todo.id === id)
if (todo === undefined) throw new Error('Not Found Todo')
return todo
}
const toggleIsCompletedTodo = (id: number) => {
const todo = searchTodoById(id)
todo.isCompleted = !todo.isCompleted
}
const todoCount = computed(() => todos.value.length)
const completedTodoCount = computed(() => todosFilteredCompleted.value.length)
const incompleteTodoCount = computed(() => todosFilteredIncomplete.value.length)
return {
todos,
addTodo,
todosFilteredCompleted,
todosFilteredIncomplete,
toggleIsCompletedTodo,
todoCount,
incompleteTodoCount,
completedTodoCount
}
})
TaskItem.vueコンポーネント
useTodoStore
からtoggleIsCompletedTodo
を呼び出す。
toggleIsCompletedTodo
はtodoが持っているisCompleted
を切り替える関数です。
<script lang="ts" setup>
import { useTodoStore } from '@/stores/todo'
defineProps<{
id: number
text: string
isCompleted: boolean
}>()
const todoStore = useTodoStore()
const { toggleIsCompletedTodo } = todoStore
</script>
<template>
<div class="card" :class="'card' + (isCompleted ? '__completed' : '__incomplete')">
<div class="card--id">id: {{ id }}</div>
<div class="card__body">
<div class="card__body--text">{{ text }}</div>
<button
@click="toggleIsCompletedTodo(id)"
class="card__body--button"
:class="'card__body--button' + (isCompleted ? '__completed' : '__incomplete')"
>
<div v-if="isCompleted">完了済</div>
<div v-else>完了</div>
</button>
</div>
</div>
</template>
<style lang="scss" scoped>
.card {
padding: 12px;
border-radius: 12px;
&__completed {
border: 1px solid hsl(160, 100%, 37%);
}
&__incomplete {
border: 1px solid #ababab;
}
&--id {
font-size: 1rem;
}
&__body {
display: flex;
justify-content: start;
align-items: center;
column-gap: 1rem;
&--text {
font-size: 2rem;
font-weight: bold;
}
&--button {
padding: 4px 14px;
cursor: pointer;
border-radius: 6px;
border: none;
&__completed {
background: hsl(160, 100%, 37%);
}
&__incomplete {
background: #ababab;
}
}
}
}
</style>
HomeView.vue
HomeView.vue
では管理しているtodo全てを表示されるようにしています。
store上の値の変更を検知しつつ参照するためにはPiniaのユーティリティ関数のstoreToRefs
を使用して値を呼び出す。
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useTodoStore } from '@/stores/todo'
import TaskItem from '@/components/TaskItem.vue'
const todoStore = useTodoStore()
const { todos } = storeToRefs(todoStore)
</script>
<template>
<div class="card__wrap">
<TaskItem
v-for="todo in todos"
:key="todo.id"
:id="todo.id"
:text="todo.text"
:is-completed="todo.isCompleted"
/>
</div>
</template>
<style lang="scss" scope></style>
IncompleteTodosView.vue
やCompletedTodosView.vue
はHomeView.vue
と同じコードで書いています。
簡単に書いたコードを説明していきましたが、実際に動かして確認をしたい方は下記のリポジトリからcloneしていただければと思います。
最後に
Piniaについて学んできましたが、VueはReactとは違い誰でも扱いやすいものだなと感じました。
今回はVueを学び直そうとした際にPiniaというものを知りましたが、公式プロジェクトになったのは約2年前であることから、情報のキャッチアップをしっかり出来ていなかったと痛感しました。
今回の経験を忘れずにVue以外の言語もキャッチアップして可能であれば記事を投稿して自分の成長に繋げていければと思います。