Vue コンポーネントの状態と URL クエリパラメータを同期させる

3 min read読了の目安(約2800字

概要

以下の gif のように、 URL のクエリパラメータと、Vue コンポーネントの状態(data) を同期させる mixin を作ったので備忘録。

demo

前提

  • vue 2.6.12
  • vue-router 3.0.2
  • TypeScript は不使用

コンポーネント側

component.vue
export default {
  mixins: [useURLQuery],
  data: () => ({
    hoge: '',
    fuga: 0
  }),
  urlSyncedData: {
    hoge: {
      type: Number
      validator: (v) => v > 0 && v < 100
    },
    fuga: {
      type: String
    }
  }
}

使い方

  • 後述の useURLQuery をミックスインに含めます
  • data で定義しているパラメータのうち、URLクエリパラメータと同期させるパラメータを、 urlSyncedData に定義します
    • type: 期待する型 (Number/String/Boolean のみ)
    • validator URLから受け取ったデータを元にしたバリデータ関数

特徴

  • URLとコンポーネントで双方向に同期するため、初期読み込み時はURLを元にコンポーネントが初期描画され、UI上で状態が切り替わると、URLも即座に変更されます
  • バリデータによって、URLに含まれているデータが不正と判定された場合は、すみやかにコンポーネント側の初期値を利用します
  • useURLQuery で定義していないパラメータについては、URLに含まれていても干渉しません

mixin 側

useURLQuery.js
export default {
  beforeMount() {
    const urlParams = this.$route.query
    Object.keys(this.urlSyncedData).forEach(key => {
      // 同期対象パラメータを監視対象にする
      this.$watch(key, _ => this.updateUrlParams())

      // URLパラメータの指定がない場合は、コンポーネント側の初期値をそのまま使う
      if (urlParams[key] === undefined) return

      // コンポーネント側で設定された、同期対象パラメータ設定を取得
      const typeConstructor = this.urlSyncedData[key].type
      const validator = this.urlSyncedData[key].validator

      // 有効なURLパラメータが設定されている場合のみ、コンポーネントの状態を初期化
      const parsedValue = this.parseRawValue(urlParams[key], typeConstructor)
      if (validator === undefined || validator(parsedValue)) {
        this.$set(this, key, parsedValue)
      }
    })
  },
  computed: {
    urlSyncedData() {
      return this.$options.urlSyncedData
    }
  },
  methods: {
    /**
     * 指定した型コンストラクタに応じて、URLパラメータ内の生データを変換する
     * @param {Number|String|Boolean} rawValue
     * @param {Function} typeConstructor
     * @param {Function=} validator
     */
    parseRawValue(rawValue, typeConstructor) {
      return typeConstructor === Boolean ? rawValue === 'true' : typeConstructor(rawValue)
    },

    /**
     * 最新のコンポーネントの状態を元に、URLパラメータを更新する
     */
    updateUrlParams() {
      const newUrlParams = {}
      Object.keys(this.urlSyncedData).forEach(key => {
        newUrlParams[key] = `${this.$data[key]}`
      })

      this.$router.replace({
        ...this.$route,
        query: {
          ...this.$route.query,
          ...newUrlParams
        }
      })
    }
  }
}

解説

  • Vue2 の Options API は純粋なオブジェクトなので、 this.urlSyncedData の形でアクセスできます
  • this.$watch this.$set によって、動的に Vue コンポーネントを拡張できます
  • this.$router.replace によって、画面遷移をせずに、シームレスに URLクエリパラメータを書き換えることができます

備考

  • ArrayObject は必要でなかったため、対応していません
    • 対応する場合は parseRawValue 関数の拡張が必要そう
  • Options API の仕組みに依存しているので、 CompositionAPI や Vue3 での使用は難しそう
  • というかよくあるユースケースなので、他にベストプラクティスがありそう