💭

Nuxt 3でComposableを活用:Vuexに頼らないシンプルな状態管理

2024/12/16に公開

これはなに?

本記事は、Nuxt 3においてVuexなどの外部ライブラリを使わず、Composableを活用して状態管理を行うベストプラクティスを紹介するものです。従来、Vue 2系のアプリケーションでは、状態管理といえばVuexがほぼデファクトスタンダードでした。しかし、Nuxt 3(+ Vue 3)の時代になると、Composition APIが標準化され、状態管理に特化した外部ツールに依存しなくても、柔軟かつシンプルにグローバルな状態を扱えるようになっています。

本記事では、実際のプロジェクトで「Vuexを使わずにComposableで全てを管理」した経験をベースに、実装例などを共有します。

前提

  • Nuxt 3 + Vue 3環境
    Composition APIが標準となり、ref, reactive, computed, watchなどの基本的な利用が可能。

  • Composableとは?
    Vue 3のComposition APIにおける状態管理・ロジック切り出し用の機能的な関数群のこと。useSomething()という名前で定義し、状態やメソッドを外部から簡潔に利用することができます。

  • なぜVuexを使わないのか?
    VuexはVue 2の時代における状態管理の標準的な解決策でしたが、以下のような課題がありました:

    • 設定や記述がやや冗長
    • ミューテーションやアクションの定義が煩雑
    • 型定義が複雑になりがち(TypeScript導入時)
    • 一方、Composableは純粋な関数・オブジェクトであり、自由度が高く、状態管理をシンプルに実装できます。また、Vue 3のリアクティビティシステムはComposableを前提とした設計になっており、外部ツールを使わずともスケールすることが可能です。

どうやるか?

基本的なパターン

  1. Composableの作成
    composables ディレクトリ配下に useXXXXのようなファイル名でComposableを定義します。
useCounter.ts
import { ref } from 'vue';

export const useCounter = () => {
  const count = ref(0);
  const increment = () => {
    count.value++;
  };
  const decrement = () => {
    count.value--;
  };
  return { count, increment, decrement };
}

  1. グローバルな状態管理としてのComposable
    Composableを通じて、複数のコンポーネント間で状態を共有したい場合は、useState ComposableやuseSharedComposableといったパターンを用いることができます。
    Nuxt 3では useState というヘルパーが用意されており、これを使うことで、アプリケーション全体で共有できる状態を簡単に作れます。
useGlobalUser.ts
import { ref, computed } from 'vue';
import { useState } from '#app';

export const useGlobalUser = () => {
  const user = useState('user', () => ({
    name: '',
    loggedIn: false,
  }));

  const isLoggedIn = computed(() => user.value.loggedIn);

  const login = (name: string) => {
    user.value.name = name;
    user.value.loggedIn = true;
  };

  const logout = () => {
    user.value.name = '';
    user.value.loggedIn = false;
  };

  return { user, isLoggedIn, login, logout };
}

これで、どのコンポーネントからでも useGlobalUser() を呼び出せば、同じuser状態を参照・操作することができます。またComposableは単純な関数であり、状態管理以外にもロジックのカプセル化が容易です。useUser() useAuth() useCart() のように機能ごとに分割することで、ドメイン別の関心事を明確にし、見通しの良いディレクトリ構成が可能です。

従来のVuexとの比較

  • Vuexではstore/index.jsにモジュールを定義し、ミューテーション・アクション・ゲッターを記述していました。
store/counter.js
export const state = () => ({
  count: 0
})

export const mutations = {
  increment(state) {
    state.count++
  },
  decrement(state) {
    state.count--
  }
}

export const getters = {
  count: (state) => state.count
}
count.vue
<template>
  <div>
    <p>現在のカウント: {{ count }}</p>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
  </div>
</template>

<script>
import { mapGetters, mapMutations } from 'vuex'

export default {
  computed: {
    ...mapGetters(['count'])
  },
  methods: {
    ...mapMutations(['increment', 'decrement'])
  }
}
</script>

このようにVuexを使う場合は、
store/ディレクトリ内で状態、ミューテーション、ゲッターを定義し、
コンポーネント側ではmapGetters, mapMutationsなどで紐づける必要があります。

  • Composableでは単純な関数とref, computedでグローバル状態を表し、React Hooksに近い直感的な書き方が可能です。
  • 設定ファイルや厳格モード設定などに悩む必要がなく、状態管理がより「Vueらしい」構文に統合できます。

補足

  1. 再利用性とスケーラビリティ
    小規模なアプリではComposableでの状態管理は非常にシンプルに機能しますが、プロジェクトが巨大化するとファイルの分散や命名規則がポイントになります。use***という命名を一貫して用いる、ドメインごと(例:useAuth, useCart, useSettingsなど)にComposableを分け、/composables配下で管理するとスケールしやすいです。

  2. 型定義と補完
    TypeScriptを用いる場合は、Composable関数の返り値や引数に明示的な型定義を行いましょう。これにより、IDEでの型補完やリファクタリングが容易になり、可読性や保守性も向上します。

  3. Vuexのようなツールのメリットも念頭に
    Vuexはより明確なパターンと厳格なフローを提供するため、巨大なチーム開発では統制が取りやすい場合もあります。Composableの場合は自由度が高い分、コーディング規約やレビューがより重要になります。

まとめ

Nuxt3時代では、Vuexのような外部ツールに頼らずとも、Composableを活用することでシンプルかつ柔軟な状態管理が可能になりました。
refやcomputedといったComposition APIの基本要素を活かし、useStateと組み合わせることで、グローバルな状態をシームレスに共有できます。

ハマりやすいポイントとしては命名規則、型定義などがありますが、それらを丁寧に扱えば、Composableによる状態管理はアプリケーションをより直感的かつ軽量なものにしてくれます。

これからNuxt 3プロジェクトを始める方や、Vuex依存から抜け出したい方は、是非Composableを使った状態管理を試してみてください!

レバテック開発部

Discussion