Nuxt 3 + TypeScript で状態管理(useState編)
Nuxt 3 の useState
を使った State 管理(状態管理)の記事です。
Nuxt 3 では Vuex がサポートされていません。
Vue 3 に対応した Vuex 5.x がリリースされるのを待たずとも Nuxt 3 にもいち早く対応した Pinia を使って State 管理をすることは可能です。
しかし、それほど大規模なアプリケーションでなければ useState
と Composables (Composition Function) を組み合わせることで非常に簡単に State 管理をすることが可能になります。
Pinia は Vue Devtools で State の変化を確認できるなど、いくつものメリットがあります。
useState()
の使い方
もっとも端的な もっとも端的に使う場合は次のように利用可能です。
<script setup lang="ts">
const counter = useState('counter', () => 0)
</script>
<template>
<div>
Counter: {{ counter }}
<button @click="counter++"> + </button>
<button @click="counter--"> - </button>
</div>
</template>
また Composable Function にすることも可能です。
export const useBasicCounter = () => useState('counter', () => 0)
ただ、このような使い方は、本格的にアプリ内で共有する値を設定するには注意が必要です。
グローバルなステートをどこからでも自由に変更ができてしまいます。
(どこからでも counter.value = 5
のように mutate できてしまう)
そのため、次のような対応を検討したいところです。
- コンポーネントでは Vuex の getter のように readonly なステートを使用したい
- 値の操作は事前に用意した関数を通じて行いたい
また、第1引数の key
を typo してしまうと、意図せず別のステートが使用されます。
これらを解決するために Composable Function を使用し Vuex や Pinia ライクな状態管理ができるようにしたいと思います。
Composable Function でリアクティブな値を利用する
まずは Composable Function の使い方の基本を説明します。
Composition API の登場により、アプリ内で利用するリアクティブな値を(Component と切り離して)準備することはとてもシンプルになりました。
import type { Ref } from 'vue'
export const inc = (counter: Ref<number>) => () => counter.value++
export const dec = (counter: Ref<number>) => () => counter.value--
export const useNotSharedCounter = () => {
const counter = ref(0)
return {
counter: readonly(counter),
inc: inc(counter),
dec: dec(counter),
}
}
このような Composable Function を Component 内で呼び出します。
<script setup lang="ts">
const { counter, inc, dec } = useNotSharedCounter()
</script>
<template>
<div>
カウンター: {{ counter }}
<button @click="inc"> + </button>
<button @click="dec"> - </button>
</div>
</template>
複数の Component から簡単に利用することが可能です。
しかし、たとえばページ遷移をするなどして Component が Unmount されると、再度 Component を呼び出した際にふたたび useNotSharedCounter()
が呼び出されるため、カウンターは初期値に戻ってしまいます。
アプリ内の状態管理をしたい場合にはこれでは用をなしません。
では、ということで useNotSharedCounter()
の外側で初期化しようとすると、サーバーサイドで値が共有されてしまうため Nuxt 3 のような ISG(SSR) が基本となるアプリケーションだと思わぬ脆弱性を生みます。
ISG で脆弱性となりうる例
次のような記述はこれまで以上に気をつける必要があります。
const counter = ref(0) // このように useCounter() の外側で初期化すると他のユーザーと共有されてしまうので脆弱性となりえます。
export const useDangerCounter = () => {
return {
counter: readonly(counter),
inc: () => counter.value++,
dec: () => counter.value--,
}
}
useState()
を使って簡単に状態管理を行う
Nuxt 3 で用意されている useState()
を Composable Function のなかで利用することで、とても簡単で、かつ安全に State をアプリ全体で扱うことが可能です。
import type { Ref } from 'vue'
export const inc = (counter: Ref<number>) => () => counter.value++
export const dec = (counter: Ref<number>) => () => counter.value--
export const useSharedCounter = () => {
const counter = useState('counter', () => (0))
return {
counter: readonly(counter),
dec: dec(counter),
inc: inc(counter),
}
}
これだけで、どの Component から呼び出しても、同じ counter
の値を利用できます。
counter
はリアクティブな値となるのでsetup()
内ではcounter.value
のように利用します。
<script setup lang="ts">
const { counter, inc, dec } = useSharedCounter()
onMounted(() => { console.log(counter.value) })
</script>
<template>
<div>
カウンター: {{ counter }}
<button @click="inc"> + </button>
<button @click="dec"> - </button>
</div>
</template>
useState()
の基本
useState()
の型定義はこのようになっています。
useState<T>(init?: () => T | Ref<T>): Ref<T>
useState<T>(key: string, init?: () => T): Ref<T>
第2引数に初期化をする関数を指定しますが、この戻り値の型が推論可能な場合は、いっさい型を意識することなく型がついた状態で利用できます。
たとえば次のような場合は string
になります。
export const useArticleTitle = () => {
const title = useState('title', () => 'No Title')
return { title }
}
任意に指定する場合は次のようになるでしょう。
export const useArticleTitle = () => {
const title = useState<string | string[]>('title', () => 'initial string')
return { title }
}
title
の型は Ref<string | string[]>
となります。
初期化関数を指定しない場合や
null
にする場合は、型を指定しないとunknown
やany
となってしまいます。
Component ではたとえば次のようになります。
<script setup lang="ts">
const { title } = useArticleTitle()
// 配列の場合は , で結合
const joined = computed(() => Array.isArray(title.value) ? title.value.join() : title.value)
onMounted(() => title.value = ['Co-Edo', 'ktanaka'])
</script>
<template>
<div>
{{ joined }}
</div>
</template>
まとめ
Nuxt 3 の利点のひとつに TypeScript フレンドリーな開発体験があります。
より簡単に扱えて、より安全に利用できる useState()
をぜひ活用していきたいですね。
Discussion