2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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 StoresSetup Stores、二種類の書き方があります。

Option Stores

Vue2まで使われていたOption APIベースで書く方法になります。
現在のVue開発ではComposition APIの方がメジャーになっているためOption APIよりもSetup Storesでpiniaを書くのが良いかと思います。

Option Stores
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を書く方がチーム開発時などでコードの統一が出来て開発がしやすくなるかと思います。

Setup Stores
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が指定されていないので指定・開発用サーバー起動時にプロジェクトが自動でブラウザで開かれるようにしていきます。

vite.config.ts
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では状態管理している値の型を管理
src/stores/todo/type.ts
export type TodoType = {
  id: number
  text: string
  isCompleted: boolean
}

書き方としては一般的なComposition APIの書き方になっています。
Composition APIと同じ書き方のため、すぐに状態管理出来るのはPiniaの強みかと思います。

src/stores/todo/index.ts
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を切り替える関数です。

src/components/TaskItem.vue
<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を使用して値を呼び出す。

src/view/HomeView.vue
<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.vueCompletedTodosView.vueHomeView.vueと同じコードで書いています。


簡単に書いたコードを説明していきましたが、実際に動かして確認をしたい方は下記のリポジトリからcloneしていただければと思います。

最後に

Piniaについて学んできましたが、VueはReactとは違い誰でも扱いやすいものだなと感じました。
今回はVueを学び直そうとした際にPiniaというものを知りましたが、公式プロジェクトになったのは約2年前であることから、情報のキャッチアップをしっかり出来ていなかったと痛感しました。
今回の経験を忘れずにVue以外の言語もキャッチアップして可能であれば記事を投稿して自分の成長に繋げていければと思います。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?