BLOG

ブログ

【NuxtJS】Vuexのサーバ・クライアント間はFunction型以外で連携しよう

お久しぶりです、ショーです。
ブログの更新が久しぶりになってしまいましたが、僕は元気です!!

業務でNuxt.js(Typescript)+Vuexを使用しているのですが、
その際に結構調査に時間がかかってしまった事象に出くわしたので、
その時の事象と解決法をご紹介します。

ちょっとマニアックな内容ですが、もし同じ事象に遭遇した時の参考にしていただければ幸いです!

はじめに

Vuexについては、Nuxt.js(Vue.js)で使える!絶対取り入れたいオススメのプラグイン
分かりやすく載っていますが、一言でいうとコンポーネント間でデータを一元管理出来るライブラリです。
SSRモードの際は、SSR・CSRのどちらも使用可能です。

※ 以降はサーバサイドレンダリングをSSR、クライアントサイドレンダリングをCSRと記載します。

動作環境

  • Nuxt:2.15.81、SSRモード
  • Typescript:4.2.4
  • Node:16.14.0
  • Vuex:3.6.2

ところで一体何があったのか

困った事象

  • とある画面で、SSRのURL直アクセス/ブラウザのスーパーリロード時に、エラー画面に遷移する
  • CSRの他画面からの画面遷移時は、正常に画面が表示される

→ この時点で、SSR時だけが怪しいと予想。

更に調査

  • ブラウザの開発者ツールを確認すると、以下のようなTypeErrorが発生している事を確認
TypeError: hoge.filter is not function
    ・・・

調査結果

上記の「hoge.filter」というfunctionがクライアントサイド側(Vue)で使いたいものですが、そのfunctionをサーバサイド(Express)で設定していました。

値変換のイメージ図

通常の数値型や文字列型などの値は、サーバ側で設定したものがそのままクライアント側に渡るのですが、Function型など特殊な型はVuexでサーバ側からクライアント側への値変換が出来ず、
結果上記のfunctionは空っぽのobjectになっていました…
(ここまで来るのに時間がかかりました)

大元はどちらもjavascriptとはいえ、厳密には言語が異なるので、少なからず言語の壁があるんだなと思いました。

解決法

今回の場合、上記のfunctionはクライアント側でしか使わないため、設定処理をクライアント側で実施するようにして解決しました。

実際にやってみた

準備

試した型は以下の通りです。
※ Symbol型は失敗に終わりましたが、一応コメントアウトでやった形跡を残してます

  • Boolean
  • Number
  • String
  • Array
  • Object
  • Function
  • Null
  • Undefined

Vuexの設定は以下の通りです。
SSR時(asyncDataなど)にactionsのfindメソッドを呼んで値を設定+ログ出力しています。

import { MutationTree, ActionTree, GetterTree, ActionContext } from 'vuex';

/** プロパティ */
const state = (): SampleState => ({
  sampleBoolean: false,
  sampleNumber: 0,
  sampleString: '',
  // sampleSymbol: {},
  sampleArray: [],
  sampleObject: {},
  sampleFunction: null,
  // 後ほどnullを設定するため、意図的に初期値を関係ない値にしている
  sampleNull: '',
  // 後ほどundefinedを設定するため、意図的に初期値を関係ない値にしている
  sampleUndefined: ''
});

/** ゲッター */
export const getters: GetterTree<SampleState, SampleState> = {
  sampleBoolean: (state) => state.sampleBoolean,
  sampleNumber: (state) => state.sampleNumber,
  sampleString: (state) => state.sampleString,
  // sampleSymbol: (state) => state.sampleSymbol,
  sampleArray: (state) => state.sampleArray,
  sampleObject: (state) => state.sampleObject,
  sampleFunction: (state) => state.sampleFunction,
  sampleNull: (state) => state.sampleNull,
  sampleUndefined: (state) => state.sampleUndefined
};

/** ミューテーション */
export const mutations: MutationTree<SampleState> = {
  setSampleValue(state: SampleState) {
    // 値設定
    state.sampleBoolean = true;
    state.sampleNumber = 999;
    state.sampleString = 'sampleString!!!';
    // state.sampleSymbol = Symbol('key');
    state.sampleArray.push('sampleArray1');
    state.sampleArray.push('sampleArray2');
    state.sampleObject = {
      id: 123,
      name: 'sampleObject!!!',
    };
    state.sampleFunction = function sampleFunction() {
      console.log('sampleFunction!!!');
      return true;
    };
    state.sampleNull = null;
    state.sampleUndefined = undefined;

    // ログ出力
    console.info('sampleBoolean: ', state.sampleBoolean);
    console.info('sampleNumber: ', state.sampleNumber);
    console.info('sampleString: ', state.sampleString);
    // console.info('sampleSymbol: ', state.sampleSymbol);
    console.info('sampleArray: ', state.sampleArray);
    console.info('sampleObject: ', state.sampleObject);
    console.info('sampleFunction: ', state.sampleFunction.toString());
    console.info('sampleNull: ', state.sampleNull);
    console.info('sampleUndefined: ', state.sampleUndefined);
  },
};

/** アクション */
export const actions: ActionTree<SampleState, SampleState> = {
  /**
   * データ取得
   */
  async find(context: ActionContext<SampleState, SampleState>) {
    context.commit('setSampleValue');
  }
};

Vue側は以下の通りで、単純にmountedでVuexのgetterを呼びつつ、ログ出力しています。
※ template、styleタグの記載は割愛

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  mounted() {
    console.log('sampleBoolean: ', this.$store.getters['sample/sampleBoolean']);
    console.log('sampleNumber: ', this.$store.getters['sample/sampleNumber']);
    console.log('sampleString: ', this.$store.getters['sample/sampleString']);
    // console.log('sampleSymbol: ', this.$store.getters['sample/sampleSymbol']);
    console.log('sampleArray: ', this.$store.getters['sample/sampleArray']);
    console.log('sampleObject: ', this.$store.getters['sample/sampleObject']);
    console.log('sampleFunction: ', this.$store.getters['sample/sampleFunction']);
    console.log('sampleNull: ', this.$store.getters['sample/sampleNull']);
    console.log('sampleUndefined: ', this.$store.getters['sample/sampleUndefined']);
  },
});
</script>

実行結果

サーバサイド処理時のログ

※ よく見たらfunctionに対してWARNログが表示されていました…
当時は気づいてませんでした…

クライアント処理時のログ

検証した型のうち、Vuexの値引き継ぎが上手くいかなかった型はFunctionのみという結果でした。

検証した型サーバサイド側
設定値
クライアント側
変換後値
変換結果
Booleantruetrue
Number999999
StringsampleString!!!sampleString!!!
ArraysampleArray1
sampleArray2
sampleArray1
sampleArray2
Objectid: 123
name: sampleObject!!!
id: 123
name: sampleObject!!!
Functionfunction sampleFunction() {
console.log(‘sampleFunction!!!’)
return true
}
空object×
Nullnullnull
Undefinedundefinedundefined
検証した型と変換結果

→ 結論、VuexでFunctonは受け渡すことができない

さいごに

事情がわかってしまえば、大したことはなかったのですが、
ただ、こういう事象に限ってなかなか原因が掴めなかったりしますので、
同じような事象で困ってる人の助けになれば幸いです!

  • この記事を書いた人
  • 最新の記事

Nakahori Shota

エンジニアからエンジニアに転職。経験的にはバックエンドが長いが、フロントエンドも魅力を感じて勉強中。 入社初日に激辛麻辣カリー湯麺の辛さ3倍を食べ、マブスの激辛王の称号を得る。(食べきりはしたが、翌日お腹がヤバかった)