LoginSignup
4

More than 1 year has passed since last update.

posted at

updated at

nuxt3のuseStateでページ遷移しても消えない状態管理。vuexもpluginも不要。

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
    >&nbsp;&nbsp;&nbsp;&nbsp;<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
    >&nbsp;&nbsp;&nbsp;&nbsp;
    <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>

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
4