8
3

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 1 year has passed since last update.

【Vue.js】watchでdeep: trueを設定するとnewValとoldValが同じになる問題

Last updated at Posted at 2023-03-29

はじめに

Vue.js(Composition API)では、watch関数を使用することで、リアクティブな状態の一部が変更されるたびにコールバックを実行することができます。

const a = ref(0)

watch(a, (newVal, oldVal) => {
  console.log(newVal, oldVal)
})

コールバック関数の第一引数(newVal)には、変更後のaの値が入り、第二引数(oldVal)には変更前のaの値が入ります。

つまり、上記のリアクティブな値a1を代入した場合、newValには1が入り、oldValには0が入ることになります。

本題

リアクティブなオブジェクトをwatchで監視する場合、下記のようになります。

const obj = ref({ a: 1, b: 2 })

watch(
  obj,
  (newVal, oldVal) => {
    console.log(newVal === oldVal);
    // => true
  },
  { deep: true }
);

deep: trueをつけることで、オブジェクトのプロパティの変更時にも、watchが変更を検知してくれるようになります。

しかしここで問題があります。

リアクティブなオブジェクトをwatchで監視する場合、コールバック関数の引数である、newValoldValの値が同じになってしまいます。

つまり、上記でobjのプロパティa3を代入した場合、newValoldValの値はどちらも{ a: 3, b: 2 }になります。(どちらもnewValの値になる)

原因として、JavaScriptではオブジェクト型の場合、プリミティブ型と違って値自体ではなく、参照をコピーするため、結果的に同じオブジェクトを参照するからです。

解決方法

解決方法としては、スプレッド構文で別の参照のオブジェクトをつくり、その値を返すgetter関数をwatchの第一引数に指定することです。

const obj = ref({ a: 1, b: 2 })

watch(
  () => ({ ...obj.value }),
  (newVal, oldVal) => {
    console.log(newVal === oldVal);
    // => false
  },
  { deep: true }
);

スプレッド構文を使うことで、オブジェクトを展開できるため、別の参照のオブジェクトをつくることができます。

1点注意点なのが、スプレット構文はディープコピーではなくシャローコピーになるので、階層の深いオブジェクトの場合は、一旦JSONにして戻したり、structuredCloneやlodashのcloneDeepを使ってディープコピーで別の参照のオブジェクトを作る必要があります。

import { cloneDeep } from "lodash";

const obj = ref({ a: 1, b: { c: 1 } })

watch(
  () => ({ ...obj.value }),
  (newVal, oldVal) => {
    console.log(newVal.b === oldVal.b);
    // => true
  },
  { deep: true }
);

watch(
  () => cloneDeep(obj.value),
  (newVal, oldVal) => {
    console.log(newVal.b === oldVal.b);
    // => false
  },
  { deep: true }
);

続いて、下記のようにオブジェクトのみを返すアロー関数の場合、カッコ()で囲むことで、returnと外側の中括弧{}を省略することができます。

// 省略なし
() => { return { ...obj.value }}

// 省略
() => ({ ...obj.value })

そして、watchではリアクティブなデータソースであれば、第一引数には下記の4パターンで定義することができます。

  • refやcomputed
  • リアクティブなオブジェクト
  • 値を返すgetter関数
  • 上記3つのいずれかの配列
const x = ref(0)
const y = ref(0)
const obj = ref({ a: 1, b: 2 })

// refやcomputed
watch(x, () => {
// 処理
})

// リアクティブなオブジェクト
watch(obj, () => {
// 処理
})

// 値を返すgetter関数
watch(
  () => x.value + y.value,
  () => {
// 処理
  }
)

// 複数ソースの配列
watch(
  [x, y, obj, () => x.value + y.value],
  () => {
// 処理
})

値を返すgetter関数を使う場合、ウォッチャーはgetter関数の戻り値が変更されたときだけ起動します。

そのため別の参照のオブジェクトをgetter関数で返すことで、newValoldValで別々の値がとれるようになります。

最後にもう一度完成コードを載せておきます。

const obj = ref({ a: 1, b: 2 })

watch(
  () => ({ ...obj.value }),
  (newVal, oldVal) => {
    console.log(newVal, oldVal);
  },
  { deep: true }
);

もっと良い方法や間違いなどあればご指摘いただけると幸いです。

参考

8
3
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
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?