9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-10-27

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>
9
5
0

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
  3. You can use dark theme
What you can do with signing up
9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?