こんにちは、かつコーチです。
前回は server/api を使って、Nuxtアプリの中に自前のAPIを作る方法を解説しました。
今回は、そのAPIから取得したデータや、ログイン情報のようにアプリ全体で使い回したい状態を管理する Pinia(Pinia:Vue公式が推奨する状態管理ライブラリ)をNuxtに導入する方法を扱います。
「複数のページやコンポーネントで同じデータを使い回したい」と感じ始めたタイミングで、ぜひ読んでほしい内容です。
Piniaとは?
アプリ全体で共有する状態を管理するライブラリ
状態管理とは、複数のコンポーネントから参照・更新される値(ログインユーザー情報やカートの中身など)を、1箇所にまとめて管理する考え方のことです。
Piniaは、この状態管理をVueの流儀に沿った書き方で実現できるライブラリで、Vuexの後継として公式に推奨されています。
Pinia自体の詳しい設計思想やストアの分割方法については、以前のVue編で解説しているので、この記事ではNuxtへの導入手順に絞って説明します。
なぜNuxtでPiniaが必要になるのか
provide / inject やコンポーネント間のProps受け渡しだけでも状態は共有できますが、階層が深くなったりページをまたいだりすると、管理が煩雑になっていきます。
Piniaを使うと、どのコンポーネントからでも同じストアにアクセスできるようになり、SSR環境でもリクエストごとに状態がリセットされる仕組みが整っているため、Nuxtとの相性がよいという特徴があります。
基本の書き方 / 実装手順
手順1:@pinia/nuxtモジュールを導入する
まずはパッケージをインストールします。
npm install pinia @pinia/nuxt
次に nuxt.config.ts に @pinia/nuxt をモジュールとして登録します。
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@pinia/nuxt"],
})
@pinia/nuxt モジュールを登録するだけで、Piniaのセットアップ処理やSSR対応が自動的に行われます。
手順2:ストアを定義する
stores ディレクトリを作り、そこにストアファイルを置きます。
// stores/counter.ts
import { defineStore } from "pinia"
export const useCounterStore = defineStore("counter", {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
defineStore の第1引数(今回は "counter")は、ストアを識別するための一意な名前です。
state は保持する値、getters は算出値、actions は状態を更新するメソッドをそれぞれ定義します。
手順3:コンポーネントからストアを使う
定義したストアは、nuxt.config.ts にモジュール登録さえしていれば、import すら書かずにコンポーネント内で使えます。
<!-- pages/index.vue -->
<script setup lang="ts">
const counterStore = useCounterStore()
</script>
<template>
<div>
<p>カウント:{{ counterStore.count }}</p>
<p>2倍:{{ counterStore.doubleCount }}</p>
<button @click="counterStore.increment">+1</button>
</div>
</template>
@pinia/nuxt モジュールが useCounterStore のような関数をNuxtの自動インポート(自動インポート:特定のディレクトリに置いたファイルを、import文なしでどこからでも使えるようにするNuxtの機能)の対象にしてくれるため、import 文を書く必要がありません。
つまずきやすい設定・注意点
ストアファイルを stores/ ディレクトリの直下に置くのがルールなので、別のディレクトリに置くと自動インポートが効かず、useCounterStore is not defined のようなエラーになります。
ファイルの置き場所を間違えていないか、まず確認するようにしましょう。
よくあるつまずきポイント・エラー対処
Before/After:ストア内でuseFetchを使ってハマった話
私が実際につまずいたのは、ストアのaction内でデータを取得しようとしたときでした。
❌ Before:ストアのaction内でuseFetchを使う
// stores/posts.ts
import { defineStore } from "pinia"
interface Post {
id: number
title: string
}
export const usePostsStore = defineStore("posts", {
state: () => ({
posts: [] as Post[],
}),
actions: {
async fetchPosts() {
// ストアのactionの中でuseFetchを使うと警告が出る
const { data } = await useFetch<Post[]>("/api/posts")
this.posts = data.value ?? []
},
},
})
このコードを実行すると、コンソールに次のような警告が表示されました。
[nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function.
useFetch はVueコンポーネントの setup 関数など、Nuxtの実行コンテキストの中で呼び出される前提のComposableです。
ストアのactionはコンポーネントの外側から呼び出されることもあるため、実行タイミングによってはこのコンテキストが失われ、警告やエラーにつながってしまいます。
✅ After:ストア内では$fetchを使う
// stores/posts.ts
import { defineStore } from "pinia"
interface Post {
id: number
title: string
}
export const usePostsStore = defineStore("posts", {
state: () => ({
posts: [] as Post[],
}),
actions: {
async fetchPosts() {
this.posts = await $fetch<Post[]>("/api/posts")
},
},
})
$fetch はNuxtの実行コンテキストに依存しないシンプルなHTTPクライアントなので、ストアのaction内から呼び出しても問題が起きません。
実務でも「ストア内のデータ取得は $fetch、コンポーネント直下でのページ初期表示用データ取得は useFetch 」という使い分けが定着しています。
応用・一歩先の使い方
Composition API形式でストアを書く
defineStore はオブジェクト形式のほかに、Composition API風の書き方にも対応しています。
// stores/counter.ts
import { defineStore } from "pinia"
export const useCounterStore = defineStore("counter", () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment(): void {
count.value++
}
return { count, doubleCount, increment }
})
ref ・ computed ・ 関数をそのまま return する形式で、普段の <script setup> の感覚に近い書き方ができます。
ストアの中身が複雑になってきたら、こちらの書き方に切り替えると見通しがよくなることがあります。
ページ遷移をまたいで状態を保持する
Piniaのストアは、ページ遷移をしても状態が保持されます(画面を再読み込みした場合はリセットされます)。
ログイン中のユーザー情報のように、複数ページで参照したい値をストアに持たせておくと、ページごとにAPIを叩き直す必要がなくなります。
まとめ
この記事のポイント
- Piniaは複数のコンポーネント・ページで共有する状態をまとめて管理するライブラリ
@pinia/nuxtをnuxt.config.tsのmodulesに追加するだけで導入できるstores/ディレクトリにファイルを置くと、自動インポートでimport文なしに使える- ストアのaction内でデータ取得する場合は、
useFetchではなく$fetchを使うのが安全 - Pinia自体の詳しい設計(ストア分割・テスト方法など)はVue編を参照するとよい
次に読むべき記事
データ取得・自作API・状態管理と一通り揃ったら、次はログイン機能そのものをNuxtに実装する方法が気になってくるはずです。
→ 次の記事:nuxt-auth-utilsで認証機能を実装する