2021/11/18追記 その1
この記事は2021/10/28当時の公式のuseState
の情報(こちら)が少なく、「pluginの中のdefineNuxtPlugin内で初期宣言しないといけないの?」と読めてしまう状態だったころに書かせていただいていました。
しかし、いつの間にか公式が更新され、わかりやすくなっています。
今となっては、この記事を読まなくても公式で十分です。
22021/11/18追記 その2
「reactive(・・・)
と組み合わせることも可」としていましたが、useState()
の返しはRef<T>
なので、そもそもreactive(・・・)
は必要ないです。
概要
- nuxt3はuseStateでグローバルかつページ遷移しても消えない状態管理が可能。(vuex不要)
- 型推論が簡単に効いて快適・・・!
公式(こちら)ではぱっと見「pluginの中のdefineNuxtPlugin内で定義」の様に読めるが、pluginの中でなくてもよい。公式はssrContextを使用しようとしているからpluginの中で定義しているだけ・・・!- ↑2021/11/18追記 いつの間にか公式が更新され、サンプルでdefineNuxtPluginなど使われなくなりました。
- 公式ではstring(プリミティブな値)を例にしていたが、もちろんオブジェクトも可能。
- 1ページ内のリアクティブなメンバーを一発で丸ごと管理して、型推論も効く・・・!
- ライフサイクルはjavascirptのグローバル変数と同じなので、ブラウザリロードなどで消える。
- これはvuexなどと同様。
実装時の要点
useState<[型]>("[識別子]", [初使用の際に初期値を返す関数(省略可)])
reactive(・・・)
と組み合わせることも可- ↑2021/11/18追記
reactive(・・・)
を使わなくてもリアクティブです。
例
const initialState = {foo:"baa", count:1};
const state = reactive(
useState<typeof initialState>("state001", () => {
return cloneDeep(initialState);
})
);
- script内で使用する場合は「.value」が必要。
例
state.value.qrText = await QRCode.toDataURL(state.value.foo);
- template内で使用する場合は「.value」をつけない。
例
<input type="text" v-model="state.foo" />
実装例
試しに以下の2ページを作り、ページ遷移しても値が保持されることを確認しました。
(リアクティブであることも確認できました。computedもwatchEffectも反応します。)
- (正規表現を試すページ) /pages/regex.vue
- (QRコードを作成するページ) /pages/qrcode.vue
/pages/regex.vue
<template>
<div>
<router-link :to="{ path: 'qrcode' }">qrcodeへ</router-link
> <router-link
:to="{ path: 'qrcode', query: { init: 1 } }"
>qrcodeへ(値初期化)</router-link
><br />
<table>
<tr>
<th>regex:</th>
<td><input type="text" v-model="state.regex" /></td>
</tr>
<tr>
<th>isGlobal:</th>
<td><input type="checkbox" v-model="state.isGlobal" /></td>
</tr>
<tr>
<th>target:</th>
<td><textarea v-model="state.target"></textarea></td>
</tr>
</table>
<br />
<table>
<tr>
<th>regex.exec(target):</th>
<td>{{ compExec }}</td>
</tr>
<tr>
<th>regex.test(target):</th>
<td>{{ compTest }}</td>
</tr>
<tr>
<th>target.match(regex):</th>
<td>{{ compMatch }}</td>
</tr>
<tr>
<th>[...target.matchAll(regex)]:</th>
<td>{{ compMatchAll }}</td>
</tr>
<tr>
<th>target.search(regex):</th>
<td>{{ compSearch }}</td>
</tr>
<tr>
<th>target.replace(regex, "{{ replaceStr }}"):</th>
<td>{{ compReplace }}</td>
</tr>
<tr>
<th>target.split(regex):</th>
<td>{{ compSplit }}</td>
</tr>
</table>
<button @click="init">初期化</button>
</div>
</template>
<style scoped>
@import url(https://fonts.xz.style/serve/inter.css);
@import url(https://cdn.jsdelivr.net/npm/@exampledev/new.css@1.1.2/new.min.css);
</style>
<script setup lang="ts">
import cloneDeep from "lodash.clonedeep"; //lodashはnuxt3に含まれている。
//初期値定義
const initialState = {
regex: "cd",
isGlobal: false,
target: "abcdefgabcdefg",
};
//初期値適応
const state = reactive(
// useRoute().path → /pages/regex.vueの場合は「/regex」(?・・・や#・・・があっても変化なし)
// ページ単位で状態管理することが目的。
useState<typeof initialState>(useRoute().path, () => {
return cloneDeep(initialState);
})
);
//初期化メソッド定義
const init = () => {
state.value = cloneDeep(initialState);
};
//パラメータinit=1があれば初期化
if (useRoute().query.init) {
init();
//init=1を削除したURLに更新(history back時に初期化されないように)
const nextQuery = cloneDeep(useRoute().query);
delete nextQuery.init;
useRouter().replace({ path: useRoute().path, query: nextQuery });
}
//以降、正規表現検証関連
const errMsg = "正規表現不正";
const replaceStr = "[---]";
const compRegObject = computed(() => {
let reg = null;
try {
reg = state.value.isGlobal
? new RegExp(state.value.regex, "g")
: new RegExp(state.value.regex);
} catch {
return null;
}
return reg;
});
const compExec = computed(() => {
return compRegObject.value
? compRegObject.value.exec(state.value.target)
: errMsg;
});
const compTest = computed(() => {
return compRegObject.value
? compRegObject.value.test(state.value.target)
: errMsg;
});
const compMatch = computed(() => {
return compRegObject.value
? state.value.target.match(compRegObject.value)
: errMsg;
});
const compMatchAll = computed(() => {
return compRegObject.value
? state.value.isGlobal
? [...state.value.target.matchAll(compRegObject.value)]
: ""
: errMsg;
});
const compSearch = computed(() => {
return compRegObject.value
? state.value.target.search(compRegObject.value)
: errMsg;
});
const compReplace = computed(() => {
return compRegObject.value
? state.value.target.replace(compRegObject.value, replaceStr)
: errMsg;
});
const compSplit = computed(() => {
return compRegObject.value
? state.value.target.split(compRegObject.value)
: errMsg;
});
</script>
/pages/qrcode.vue
<div>
<router-link :to="{ path: 'regex' }">regexへ</router-link
>
<router-link :to="{ path: 'regex', query: { init: 1 } }"
>regexへ(値初期化)</router-link
>
<br />
<input type="text" v-model="state.text" /><br />
<img v-if="state.qrText" :src="state.qrText" alt="QRcode" /><br />
<button @click="init">初期化</button>
{{ state.qrText }}
</div>
</template>
<style scoped>
@import url(https://fonts.xz.style/serve/inter.css);
@import url(https://cdn.jsdelivr.net/npm/@exampledev/new.css@1.1.2/new.min.css);
</style>
<script setup lang="ts">
import cloneDeep from "lodash.clonedeep";
import QRCode from "qrcode"; //qrcodeが必要。yarn add qrcode
//初期値定義
const initialState = {
text: "この文字がQRコード化",
qrText: "",
};
//初期値適応
const state = reactive(
// useRoute().path → /pages/qrcode.vueの場合は「/qrcode」(?・・・や#・・・があっても変化なし)
// ページ単位で状態管理することが目的。
useState<typeof initialState>(useRoute().path, () => {
return cloneDeep(initialState);
})
);
//初期化メソッド定義
const init = () => {
state.value = cloneDeep(initialState);
};
//パラメータinit=1があれば初期化
if (useRoute().query.init) {
init();
const nextQuery = cloneDeep(useRoute().query);
delete nextQuery.init;
useRouter().replace({ path: useRoute().path, query: nextQuery });
}
watchEffect(async () => {
state.value.qrText = await QRCode.toDataURL(state.value.text);
});
</script>