Written by TSUYOSHI

Vueでオブジェクトのdataがリアクティブに更新されない時の対処方法

JavaScript PROGRAMMING Vue

本記事では、Vueでデータはきちんと更新されているのに、仮想DOMの表示に反映されない…といった時の解決方法について解説します。

この記事を書いている僕は、フロントエンドのプログラマー(フリーランス)として、これまで4年ほどのWeb制作経験があり、Vue,React,Nuxtなどフロントエンドの開発を得意としています。

この記事を読むことによって、オブジェクトのdataをリアクティブに更新する方法がわかるようになります。

Vueでオブジェクトのdataがリアクティブに更新されない時の対処方法


Vueでオブジェクトのデータを非同期のAjax通信などで取得してDOMに反映する時、データは確かに入っているはずなのに、DOMに反映されないといった現象が発生する時があります。これはデータをリアクティブに設定できていないことが原因だったりします。

詳細を具体例を見ながら解説していきます。

オブジェクトや配列のdataはVue.setで更新する

結論からいうと、オブジェクト変数の更新は、this.$set (Vue.setのエイリアス)を使わないとリアクティブに更新されないというVueのdataを扱う場合の性質があります。

以下に具体例をあげます。
Vueのコンポーネントで、「データ取得ボタン1」「データ取得ボタン2」が表示されている表示を作ります。

ボタンを押すとそれぞれAjaxにて、APIからJSON形式のテキストデータを取得して、Vueのコンポーネント上に表示させるようなものを作ります。

GitHub

紹介するコードは、GitHubにサンプルの全ソースコードを上げています。
https://github.com/it-web-life/vue_object_not_updated

動作確認方法

サンプルコードを動作させる時は以下の手順で確認できます。
VueとExpressの両方を起動させないと正常に動作しません。Expressについては、「【簡単】Express + Node.jsの環境で簡単にAPIを作成する方法」にて、基礎を解説しているので分からない方は参考にしてみてください。

▼Vueの起動方法

  • GitHubからソースコードをgit cloneして持ってくる
  • ターミナル画面を開いて、今回cloneしたディレクトリに移動する
  • ルートディレクトリ(Vueコード)にて、yarn install
  • ルートディレクトリ(Vueコード)にて、yarn serve
  • Vueが起動する
    http://localhost:8080 にアクセスして表示が確認できる

▼API(Express)の起動方法

  • 別の新しいターミナル画面を開く
  • cloneしたルートディレクトリ→expressディレクトリに移動 (cd express)
  • expressディレクトリにて、npm install
  • expressディレクトリにて、node app.js
  • APIが起動する
    http://localhost:3000 でAPIの起動が確認できる

※停止するときは、「control + c」で停止できます。

Vueコンポーネントのソースコード

ソースコードは以下の通りです。

▼Vueコンポーネント(src/components/ContentText.vue)のソースコード

<template>
  <div class="content">
    <template v-if="Object.keys(textContent).length">
      <!-- データ取得ボタン1で取得したデータの表示領域 -->
      <div
        v-if="textContent.textObject1"
        class="item"
      >
        <h2>{{ textContent.textObject1.title }}</h2>
        <p class="subtitle">{{ textContent.textObject1.subTitle }}</p>
        <p>
          {{ textContent.textObject1.bodyText }}
        </p>
      </div>

      <!-- データ取得ボタン2で取得したデータの表示領域 -->
      <div
        v-if="textContent.textObject2"
        class="item"
      >
        <h2>{{ textContent.textObject2.title }}</h2>
        <p class="subtitle">{{ textContent.textObject2.subTitle }}</p>
        <p>
          {{ textContent.textObject2.bodyText }}
        </p>
      </div>
    </template>

    <div><button @click = "onClick1">データ取得ボタン1</button></div>
    <div><button @click = "onClick2">データ取得ボタン2</button></div>
  </div>
</template>
<script>
import axios from 'axios';

export default {
  name: 'ContentText',
  data: function() {
    return {
      textContent: {},
    }
  },
  methods: {
    async getData1() {
      // APIからobjectdata1を取得
      const { data } = await axios.get('http://localhost:3000/api/objectdata1');
      console.log('data1', data);

      if (data) {
        // this.$setを使ってリアクティブにする
        this.$set(this.textContent, 'textObject1', JSON.parse(JSON.stringify(data)));
      }

      console.log('this.textContent', this.textContent);
    },
    async getData2() {
      // APIからobjectdata2を取得
      const { data } = await axios.get('http://localhost:3000/api/objectdata2');
      console.log('data2', data);

      if (data) {
        // this.$setを使ってリアクティブにする
        this.textContent.textObject2 = JSON.parse(JSON.stringify(data));
      }

      console.log('this.textContent', this.textContent);
    },
    // データ取得ボタン1のクリックイベント
    onClick1 () {
      this.getData1();
    },
    // データ取得ボタン2のクリックイベント
    onClick2 () {
      this.getData2();
    }
  }
}
</script>

▼ソースコードの解説

ContentText.vue」コンポーネントにロジックをまとめており、起点となるApp.vueから呼ばれていて、localhost:8080にアクセスすれば表示されるようになってます。

dataの「textContent」で表示をコントロールしています。

  data: function() {
    return {
      textContent: {},
    }
  },

「データ取得ボタン1」の挙動 ※「this.$set」で正常に更新されるパターン

「データ取得ボタン1」をクリックすると、「onClick1()」メソッドが呼ばれ、さらに「getData1()」メソッドが呼ばれるようになっています。
http://localhost:3000/api/objectdata1 からデータを非同期で取得して、「this.$set」を使って表示更新をします。

「データ取得ボタン2」の挙動 ※表示更新されないパターン

同様に、「データ取得ボタン2」をクリックすると、「onClick2()」メソッドが呼ばれ、さらに「getData2()」メソッドが呼ばれます。
http://localhost:3000/api/objectdata2 からデータを非同期で取得して、直接データを代入しますが、リアクティブでないため、表示が更新されません。

両方を見ていただくと分かるように、「データ取得ボタン1」クリック動作で、「this.$set」を使うと表示が反映されますが、「データ取得ボタン2」はクリックしても表示が更新されません。
※ページリロードして1回目のクリックにて確認してみてください。

これはボタンクリックの処理で最後にconsole.log()で「this.textContent」の中身を表示しているのを見るとよく分かります。
▼「データ取得ボタン1」クリック時
title , subTitle, bodyText の各プロパティにgetter, setterが設定されてリアクティブになっています。これはオブジェクトのデータを「this.$set」で設定しているので、title , subTitle, bodyText がそれぞれリアクティブになっていて正常に動作することがわかります。

▼「データ取得ボタン2」クリック時
title , subTitle, bodyText の各プロパティのgetter, setterが設定されていないことがわかります。つまりリアクティブになっていないため、仮想DOMに表示が反映されないです。

これらを見ると分かりますが、data変数のオブジェクトは「this.$set」で更新しなければなりません。

Expressコンポーネントのソースコード
一応、Expressのソースコードも軽く紹介しておきます。

簡易的なAPIをExpressでサーバーを立てて、localhost:3000にアクセスするようにしています。

単純に「localhost:3000/api/objectdata1」「localhost:3000/api/objectdata2」にアクセスした時にオブジェクトのデータを返しているだけのものになります。

▼Express(express/app.js)のソースコード

const express = require("express");

const app = express();

// 返却データ
const responseObjectData1 = {
  title: 'Objectのタイトル1です',
  subTitle: 'Objectのサブタイトル1です',
  bodyText: 'Objectの本文1です'
};
const responseObjectData2 = {
  title: 'Objectのタイトル2です',
  subTitle: 'Objectのサブタイトル2です',
  bodyText: 'Objectの本文2です'
};

app.get("/", (req, res) => {
  res.status(200).send("Express!!");
});

app.get("/api/objectdata1", function(req, res, next) {
  // CORS対応
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
  res.header(
    'Access-Control-Allow-Headers',
    'Content-Type, Authorization, access_token'
  );

  // データの返却
  res.json(responseObjectData1);
});

app.get("/api/objectdata2", function(req, res, next) {
  // CORS対応
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
  res.header(
    'Access-Control-Allow-Headers',
    'Content-Type, Authorization, access_token'
  );

  // データの返却
  res.json(responseObjectData2);
});

// ポート3000番でlistenする
app.listen(3000);

どうしても更新されなければ$forceUpdate()で更新する

this.$setを使えば解決できますが、複雑なデータ構成になっていて、正常に更新できないケースもあります。こういった場合は、「this.$forceUpdate()」で強制的に仮想DOMの更新をすることができます。

例えば、今回のVueコンポーネント(ContentText.vue)サンプルの「getData2()」の処理の最後に、「this.$forceUpdate();」を入れると「データ取得ボタン2」をクリックした時の処理でも表示更新がされるようになります。

getData2()this.$forceUpdate() を追加した例

    async getData2() {
      // APIからobjectdata2を取得
      const { data } = await axios.get('http://localhost:3000/api/objectdata2');
      console.log('data2', data);

      if (data) {
        // this.$setを使ってリアクティブにする
        this.textContent.textObject2 = JSON.parse(JSON.stringify(data));
      }

      // 表示を強制更新する
      this.$forceUpdate();
      console.log('this.textContent', this.textContent);
    },

まとめ

Vueのdata変数にて、オブジェクトデータの更新をする際は「this.$set」を使って更新をするようにすべきという内容でした。

こうしないとリアクティブにデータの反映がされなくなり、バグにつながるので注意するようにしてください。

ご参考になれば幸いです。