VueからNuxtへの移行を解説する連載の第3弾です。第1弾の「ルーティング」、第2弾の「データフェッチ」に続き、今回は「状態管理」に焦点を当てて解説します。 Nuxt.jsでアプリケーションを構築する上で、状態管理は重要なテーマの一つです。コンポーネント間でのデータ共有、UIの状態維持、サーバーから取得したデータの保持など、その役割は多岐にわたります。
本稿では、Nuxtが提供する2つの主要な状態管理ツール、useState
とPinia
の役割を明確にし、それらをどのように使い分けるべきかを解説します。 さらに、認証のような実践的な設計パターンまで、具体的なコードと共に掘り下げていきます。 本稿が、既存のVue資産を活かしつつ、より堅牢でパフォーマンスの高いアプリケーションへと進化させるための一助となれば幸いです。
はじめに
本記事は下記第1弾をはじめとするVue to Nuxtの連載シリーズです。
ぜひ順に読んでみてください。
Nuxtの状態管理ツール
Nuxtには、SSRフレンドリーな状態管理のために、useState
とPinia
という2つの主要なツールが用意されています。 それぞれの役割とユースケースを理解し、適切に使い分けることが、堅牢なアプリケーション設計の鍵となります。
コンポーザブルによるシンプルな状態共有
Nuxtでは、composables
ディレクトリに状態管理のための関数(コンポーザブル)を配置するのが一般的なパターンです。 この方法の利点は、関心事を特定のファイルにカプセル化し、アプリケーションのどこからでも再利用可能な形で状態を管理できる点にあります。
このパターンの中心となるのが、Nuxtに組み込まれたuseState
コンポーザブルです。 これはコンポーネントツリーを越えてリアクティブな状態を共有するための基本的なツールで、サーバーで作成された状態がクライアントに引き継がれることを保証します。
useState
が担うのは、複雑なロジックを伴わないシンプルな状態の共有です。 主な用途として、サイドバーの開閉状態といったUIの状態管理が挙げられます。
export const useSidebarOpen = () => useState<boolean>('isSidebarOpen', () => false)
<script setup lang="ts">
const isSidebarOpen = useSidebarOpen()
</script>
<template>
<button @click="isSidebarOpen = !isSidebarOpen">Toggle Sidebar</button>
</template>
さらに、useState
は他のコンポーザブルと組み合わせることで、より高度な機能を実現できます。 例えば、useCookie
と組み合わせて、UIの状態をブラウザに永続化させてみましょう。 以下の例は、ダークモードのON/OFFをクッキーに保存するコンポーザブルです。
export const useDarkMode = () => {
// 1. useStateでリアクティブな状態を定義
const isDarkMode = useState<boolean>('darkMode', () => false)
// 2. useCookieでブラウザに状態を永続化
const darkModeCookie = useCookie<boolean>('darkMode', {
// サーバーサイドでもクッキーの値を読み取れるようにする
default: () => isDarkMode.value,
watch: true // isDarkModeの値の変更を監視してクッキーを更新
})
// 3. useStateの値をクッキーの値と同期
isDarkMode.value = darkModeCookie.value
return isDarkMode
}
このようにコンポーザブルを組み合わせることで、関心事を分離しつつ、再利用性の高い状態管理を構築できます。
Piniaストアによるグローバルな状態管理
Piniaは、Vueの公式状態管理ライブラリであり、Nuxtでも第一級のサポートを受けています。 Vuexの後継として、よりシンプルで型安全なAPIを提供し、複雑なアプリケーションの状態を体系的に管理するのに最適です。
Piniaが担うのは、アプリケーション全体で共有される、より構造化された状態ストアの作成です。 State、Getters、Actionsを組み合わせることでビジネスロジックをカプセル化できます。 そのため、ユーザー情報や認証状態、ショッピングカートの内容といった、より複雑なデータセットの管理に向いています。
import { defineStore } from 'pinia'
interface UserProfile {
id: string;
name: string;
bio: string;
}
export const useUserStore = defineStore('user', () => {
const profile = ref<UserProfile | null>(null)
function setProfile(newProfile: UserProfile) {
profile.value = newProfile
}
function clearProfile() {
profile.value = null
}
return { profile, setProfile, clearProfile }
})
これらの使い分けには、明確な指針があります。
状態が単一の値で、それに対する複雑なビジネスロジックが不要な場合はuseState
が適しています。 コンポーザブルとして切り出すことで、特定の関心事に閉じた状態を手軽に管理できます。
対照的に、複数のプロパティを持つオブジェクトや、状態を変更するための複数のメソッド(アクション)が必要な場合はPiniaを選びましょう。 状態とその操作を一つのストアにまとめることで、コードの見通しが良くなり、メンテナンス性が向上します。
シンプルで局所的ならuseState
、複雑でグローバルならPiniaと考えると良いでしょう。
認証状態を管理する実践的パターン
Nuxtにおける堅牢な認証は、HttpOnly
クッキーと状態管理の組み合わせによって実現されます。 これは、PiniaストアとNuxtのデータ取得機能を組み合わせた、より高度で実践的なパターンです。
フローは以下の通りです。
ユーザーがログインすると、サーバーはセッショントークンをHttpOnlyクッキーにセットして返します。HttpOnly属性により、ブラウザのJavaScriptからクッキーにアクセスできなくなります。
「HttpOnly属性だから安全」とはなりません。あくまでブラウザ上からのjavascript実行で呼び出せなくなるだけなので、その他の攻撃手段によりまだまだ安全ではないと言えます。
次に、アプリケーションのルートコンポーネントである app.vue
で、Nuxtのデータ取得機能(useAsyncData
など)を使います。app.vue
は全ページで最初に実行されるため、ここでセッションクッキーを元にサーバーAPIからユーザー情報を取得します。
取得されたユーザー情報はPiniaのuser
ストアに格納されます。
アプリケーション内のどのコンポーネントもこのストアを参照することで、一貫したログイン状態を判断できるようになります。
このフローにより、サーバーはリクエストのたびにクッキーでユーザーを認識できるため、最初から正しいログイン状態でページをレンダリングでき、UIのちらつき(ログイン前に一瞬だけ「ログインしてください」と表示されるなど)が発生しません。
<script setup lang="ts">
import { useUserStore } from '~/stores/user'
const userStore = useUserStore()
// アプリケーションの初期ロード時に一度だけ実行
await useAsyncData('session', async () => {
try {
const user = await $fetch('/api/auth/me')
if (user) userStore.setProfile(user)
} catch {
userStore.clearProfile()
}
})
</script>
このapp.vue
での初期化処理により、userStore
には常に最新のユーザー情報が格納されるようになります。
これで、各コンポーネントは安心してuserStore
を参照して、UIを切り替えることができます。 例えば、ヘッダーコンポーネントは次のようになります。
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useUserStore } from '~/stores/user'
const userStore = useUserStore()
const { profile } = storeToRefs(userStore) // リアクティビティを維持して分割代入
</script>
<template>
<header>
<nav v-if="profile">
<p>ようこそ、{{ profile.name }}さん</p>
<!-- 他のナビゲーションリンク -->
</nav>
<nav v-else>
<a href="/login">ログイン</a>
</nav>
</header>
</template>
storeToRefs
はPiniaからインポートするヘルパー関数で、ストアのプロパティをリアクティブなref
として取り出すために使います。 これにより、分割代入してもリアクティビティが失われません。
移行戦略ガイド:Vuexからの移行
既存のVueアプリケーション(特にVuexを利用している場合)からNuxtへ状態管理を移行するための、より具体的な戦略をコード例と共に紹介します。 Vuexの各機能が、Nuxtのどの機能に置き換わるのかをステップ・バイ・ステップで見ていきましょう。
ステップ1:UI状態の移行
Vuexで管理されがちなUIに関するシンプルな状態は、Nuxtではcomposables
に切り出すのが最適です。
移行前(Vuex) Vuexでは、state、mutations、actionsにまたがってUIの状態を管理していました。
export const state = () => ({
isSidebarOpen: false,
})
export const mutations = {
SET_SIDEBAR_OPEN(state, isOpen) {
state.isSidebarOpen = isOpen
},
}
export const actions = {
toggleSidebar({ commit, state }) {
commit('SET_SIDEBAR_OPEN', !state.isSidebarOpen)
},
}
移行後(Nuxt) この責務は、useState
を使った単一のコンポーザブルファイルに集約できます。
export const useSidebarOpen = () => useState('isSidebarOpen', () => false)
コンポーネントからは const isSidebarOpen = useSidebarOpen()
のように呼び出し、isSidebarOpen.value = !isSidebarOpen.value
のように直接状態を更新します。
ステップ2:グローバル状態の移行
アプリケーション全体で共有される、より複雑なデータ(ユーザー情報など)はPiniaストアに移行します。
移行前(Vuex)
export const state = () => ({
user: null,
})
export const mutations = {
SET_USER(state, user) {
state.user = user
},
}
移行後(Nuxt) この状態は、stores
ディレクトリ内のPiniaストアで管理します。
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
const profile = ref(null)
function setProfile(newProfile) {
profile.value = newProfile
}
return { profile, setProfile }
})
ステップ3:データ取得アクションの移行
Vuexのアクションに書かれていたデータ取得ロジックは、Nuxtのデータ取得機能(主にuseAsyncData
)に移管します。
移行前(Vuex)
export const actions = {
async fetchUser({ commit }) {
const user = await this.$axios.$get('/api/me')
commit('SET_USER', user)
},
}
移行後(Nuxt) アプリケーション起動時に必要なグローバルなデータは、app.vue
で取得し、Piniaストアに格納するのが定石です。
import { useUserStore } from '~/stores/user'
const userStore = useUserStore()
await useAsyncData('session', async () => {
const user = await $fetch('/api/me')
userStore.setProfile(user)
})
このパターンは、前の「認証状態を管理する実践的パターン」で解説した通り、SSRと連携してちらつきのないUIを実現する上で非常に重要です。
このように、Vuexが一つにまとめていた責務を、Nuxtのアーキテクチャ(composables
, Pinia
, useAsyncData
)に適切に分散させることで、より関心事が分離され、Nuxtのパフォーマンスを最大限に活かせるアプリケーションへと進化させることができます。
まとめ
本稿では、VueからNuxtへの移行における「状態管理」をテーマに、その考え方から具体的な移行戦略までを掘り下げてきました。
Nuxtの状態管理の核心は、責務の分離にあります。 Vuexが一つにまとめていた状態管理の責務を、Nuxtのアーキテクチャに合わせて適切に分散させることが重要です。
具体的には、useState
を中心としたコンポーザブルでシンプルな状態を共有しつつ、ユーザー情報のようなアプリケーション全体に関わる複雑な状態はPiniaストアでグローバルに管理するという、2つのパターンを使い分けることが鍵となります。
さらに、データ取得のロジックを状態管理から切り離し、useAsyncData
のようなNuxtのデータ取得機能に任せることで、SSRと調和したパフォーマンスの高いアプリケーションが実現します。 「認証状態を管理する実践的パターン」や「Vuexからの移行ガイド」で示したように、このアーキテクチャは、ちらつきのないUIや関心の分離といった、多くのメリットをもたらします。
Nuxtへの移行は単なるライブラリの置き換えではありません。 アプリケーションのアーキテクチャをより洗練させ、スケーラブルで高品質なWebサービスを構築するための確かな一歩となるでしょう。
この記事は役に立ちましたか?
もし参考になりましたら、下記のボタンで教えてください。
コメント