[Vue.js] 配列を毎回置換する際のrefとreactiveの使い方

IT

Vue.jsを触り始めて数日、なんとなく全体像はつかめたので次は外部APIを呼び出しその結果を表示する処理を実装しました。その際に、うまくレンダリングされずに苦労しました。そこで、今回はrefとreactiveをどのように使用すればいいかについて、実際のデータを使いながら動作を確認していきたいと思います。

※リアクティブな動作を実現する方法は他にもあると思います。また、よりよい方法がありましたら是非教えていただけたらと思います。

結論

やりたいことは次の通りです。

APIを実行して取得した結果(オブジェクトの配列)を画面に表示する。
  • refの場合

refで変数を初期化したら、あとはその変数に取得した配列のデータを代入するだけです。

<script  setup lang="ts">
import { ref } from 'vue'
import axios from 'axios';

let count = ref(0)
// 取得したデータを格納する配列を定義
type SpyInfo = { name: string }
let spies= ref<SpyInfo[]>([])

const getSpies = async () => {
    await axios.get<SpyInfo[]>('url')
        .then(r => {
            // refで定義しているのでvalueに取得した配列をそのまま代入
            spies.value = r.data
        })
        .catch(error => console.log(error))
}
</script>

<template>
    <div>
        <input type="text" v-model="count">
        <button @click="getSpies">GET</button>
        <div v-for="spyin spies">{{ spyin .name }}</div>
    </div>
</template>
  • reactiveの場合

reactiveで変数を初期化した場合は、refのように配列を代入してしまうとその時点で追跡されなくなってしまいます。そのためミューテーションメソッド(後程説明)を使用して、変数を一度空にしてから新しいデータを格納します。

<script  setup lang="ts">
import { reactive } from 'vue'
import axios from 'axios';

let count = ref(0)
// 取得したデータを格納する配列を定義
type SpyInfo = { name: string }
let spies = reactive<SpyInfo[]>([])

const getSpies = async () => {
    await axios.get<SpyInfo[]>('url')
        .then(r => {
            // splice(0)で中身を空にして、pushで追加
            spies.splice(0)
            spies.push(...r.data)
        })
        .catch(error => console.log(error))
}
</script>

<template>
    <div>
        <input type="text" v-model="count">
        <button @click="getSpies">GET</button>
        <div v-for="spyin spies">{{ spyin .name }}</div>
    </div>
</template>

前提条件と注意事項

前提条件

nodev18.17.0
npm9.8.1
Vue3.3.4

注意事項

  • 必要最小限のコードのみ記載しています
  • refやreactiveなどの用語自体の説明は省略しています。

やりたかったこと

GETボタンを押下したら、外部APIを呼び出し、取得した結果を画面に表示します。

裏で動いているAPIは以下の通りで、パスの末尾に指定した人数(最大8人)だけデータを取得するシンプルなものです。ちなみにPythonで作成してローカルで起動させています。

失敗パターン

まずは、失敗パターンを見ていきます。下記コードのように、reactiveを用いて配列の変数を定義します。その変数に、取得したデータを14行目でそのまま代入していました。この方法だといくらボタンを押してもUIは更新されません。リアクティブになっていませんでした。

<script  setup lang="ts">
import { reactive } from 'vue'
import axios from 'axios';

let count = ref(0)
// 取得したデータを格納する配列を定義
type SpyInfo = { name: string }
let spies = reactive<SpyInfo[]>([])

const getSpies = async () => {
    await axios.get<SpyInfo[]>('url')
        .then(r => {
            // reactiveで初期化した変数に取得したデータを代入
            spies = r.data
        })
        .catch(error => console.log(error))
}
</script>

<template>
    <div>
        <input type="text" v-model="count">
        <button @click="getSpies">GET</button>
        <div v-for="spyin spies">{{ spyin .name }}</div>
    </div>
</template>

失敗原因

reactiveを使用するとProxyが返されます。もともとのデータ([]:空の配列)はProxy経由で操作します。14行目ではProxy型のspies変数に配列を直接代入しているため、spies変数はProxyではなくなってしまいます。この時点でVueのリアクティビティーに追跡されなくなります。

またVue.js公式ドキュメントのreactiveの制限には、reactiveを使用した場合はオブジェクト全体を置換できないと記載されています。「spies = reactive(r.data)」のように再度reactiveを使用して、spies変数にデータを代入してもうまくいきません。

成功例

  • refの場合

refで変数を初期化したらvalueプロパティに直接取得した配列データを格納することで、リアクティブな動作を実現できます。シンプルに記述することができます。

// コード全体は結論に記載しているため、ここでは一部のみ

type SpyInfo = { name: string }
let spies= ref<SpyInfo[]>([])

const getSpies = async () => {
    await axios.get<SpyInfo[]>('url')
        .then(r => {
            spies.value = r.data
        })
}
  • reactiveの場合

まず、Vue公式ドキュメントにも記載がありますが、ミューテーションメソッドと呼ばれるメソッドを使用して配列を操作することで、リアクティブを実現できます。下記のように、10行目のspliceメソッドで配列を空にしてから11行目のpushメソッドで新しいデータを格納しています。

// コード全体は結論に記載しているため、ここでは一部のみ

type SpyInfo = { name: string }
let spies = reactive<SpyInfo[]>([])

const getSpies = async () => {
    await axios.get<SpyInfo[]>('url')
        .then(r => {
            // splice(0)で中身を空にして、pushで追加
            spies.splice(0)
            spies.push(...r.data)
        })
}

配列のレンダリングの詳細や、その他のVueが変更を検出するために使用できるミューテーションメソッドは以下をご確認ください。

おまけ

実は失敗例の(reactive)方法や、refやreactiveを使用せずにただの配列を使った方法でも、一見リアクティブに動作しているように見えることがあります。以下のコードを見てみましょう。

テキストボックスの下に取得した人数を表示する処理を追加しています。6行目の人数を格納するnumOfSpy変数はrefで定義していてリアクティブです。失敗例と同じように21行目でreactiveで定義したspies変数に直接取得したデータを格納してしまっているため、spies変数のリアクティブ性は消えてしまっているはずです。しかし、実際に動かしてみると人数は当然変わりますが、配列のデータも同時に変更されてリアクティブであるかのように動作します。また、spies変数をrefもreactiveも使わずにただの変数として定義しても、まるでリアクティブであるかのように画面が更新されます。

<script  setup lang="ts">
import { ref, reactive } from 'vue'
import axios from 'axios';

// 人数を表示するための変数(リアクティブ)
let numOfSpy = ref(0)

type SpyInfo = { name: string }
let spies = reactive<SpyInfo[]>([])

// ただの変数でも同様に動作する
// let spies: SpyInfo[] = []

const getSpies = async () => {

    await axios.get<SpyInfo[]>(`http://localhost:8000/${count.value}`)
        .then(r => {
            // 取得した人数を変更
            numOfSpy.value = r.data.length
            // Proxyに直接代入してしまっているためリアクティブ性は消えている
            spies = r.data
        })
        .catch(error => console.log(error))
}
</script>

<template>
    <div>
        <input type="text" v-model="count">
        <button @click="getSpies">GET</button>
        <div>{{ numOfSpy }}人取得</div>
        <div v-for="spy in spies">{{ spy.name }}</div>
    </div>
</template>

Vueは仮想DOMという仕組みを使い効率的に描画することができます。この仕組みのおかげで変更箇所のみを画面に反映できます。上記のコードでは、「xx人取得」の個所がリアクティブであり、配列の個所はリアクティブではありません。しかし、人数が変更されることでVueはその変更を検知して仮想DOMを再構築します。そしてもともと持っていた古い仮想DOMと比較して、差分のみを反映します。この時配列の個所も裏側ではデータが変更されているため差分として検知され、結果的に人数が変更されるのと同じタイミングで配列データも画面に反映されます。

ただし、あくまで配列データを格納するspies変数はリアクティブではありません。他のリアクティブな変数に依存してついでに再描画されているだけです。そのため、思わぬところで不具合が起きる可能性もあります。ですので、リアクティブにしたい個所はちゃんとリアクティブに記述する必要があると思います。

コメント