初期値として定義したオブジェクトをrefにそのまま渡して使用していたところ、refで定義した変数を初期化する際に、再代入をしても期待どおりに初期状態へ戻らないという問題に遭遇しました。
なんとなく「refに渡せば新しく状態が作られるのでは?」というあいまいな理解のまま使っていましたが、実際にはオブジェクトの参照が共有されていることが原因で、初期化が正しく行われていない状態になっていました。
この記事では、自分の理解整理も兼ねて次の3パターンの挙動をまとめます。
- オブジェクトをそのまま渡す
- スプレッド構文で渡す
- 毎回新しいオブジェクトを生成する
※ Vue特有の話というよりはJavaScriptの参照の性質による話がメインです。
オブジェクトをそのまま渡した場合
以下のようなフォームの例を考えます。
初期値として定義したオブジェクト(DEFAULT_PROFILE)をrefにそのまま渡して
profileDataを定義しました。
<script setup>
import { ref } from 'vue';
// 初期値としてオブジェクトを定義
const DEFAULT_PROFILE = {
name: '',
contact: { email: '', phone: ''},
skills: [],
}
const profileData = ref(DEFAULT_PROFILE); // そのまま渡す
<script>
profileDataを初期化する場合、以下のように
DEFAULT_PROFILEを再代入すれば初期状態に戻せそうな気がします。
<script setup>
profileData.value = DEFAULT_PROFILE
</script>
しかし、これでは正しく初期化されません。
参照が共有されている
JavaScriptではオブジェクトを代入する場合、値ではなく参照が渡されます。
つまり、ref(DEFAULT_PROFILE)と定義した場合にも参照が渡され、以下の2つは同じオブジェクトを参照することになります。
DEFAULT_PROFILEProfileData.value
そのため、フォーム入力によってprofileDataの内容が変更されると、
DEFAULT_PROFILEも同時に書き換わってしまいます。
実際にprofileDataとDEFAULT_PROFILEの内容を
画面上に表示して確認してみると、
フォーム入力によって両方の値が更新されていることが分かります。
スプレッド構文で渡した場合
スプレッド構文で渡す場合はどうなるか試しました。
<script setup>
const DEFAULT_PROFILE = {
name: '',
contact: { email: '', phone: ''},
skills: [],
}
const profileData = ref({ ...DEFAULT_PROFILE })
</script>
メールアドレスと電話番号だけ参照が共有される、といった挙動になりました。
スプレッド構文は浅いコピー
スプレッド構文がコピーするのは1階層目だけで、ネストされたオブジェクトや配列はDEFAULT_PROFILEの参照が渡されます。
- name:値がコピーされる
- contact:
DEFAULT_PROFILE.contactの参照が渡される - skills:
DEFAULT_PROFILE.skillsの参照が渡される
しかし、上記の結果を見る限り、配列で管理しているはずのスキルはDEFAULT_PROFILEと参照を共有されていないように見えます。
skillsの参照が共有されていない?
以下のように、toRawを使用してリアクティブになる前の元のオブジェクトを取得し、DEFAULT_PROFILEと一致するか(isSameObject)を確認しました。
<script setup>
import { toRaw, computed } from 'vue'
const isSameObject = computed(() => {
return toRaw(profileData.value.skills) === DEFAULT_PROFILE.skills
})
</script>
結果、初期状態では参照が共有されていましたが、チェックボックスを選択した際に参照が切れることがわかりました。
Vueの公式ドキュメントでは、チェックボックス操作時の配列の更新方法は見当たりませんでしたが、今回の挙動を見る限り、チェックボックスを選択したタイミングで新しい配列が再代入されているようにも見えます。
(もし詳しい挙動をご存知の方がいらっしゃいましたら、教えていただけると嬉しいです。)
毎回新しいオブジェクトを生成する
- オブジェクトをそのまま
refに渡す - スプレッド構文で浅くコピーする
といった方法では、意図せず参照が共有される可能性があります。
そのため、フォームの初期化する際は毎回新しいオブジェクトを生成する方法が安全です。
(このように、新しいオブジェクトを生成して返す関数をfactory関数と言います。)
<script setup>
const defaultProfile = () => ({
name: '',
contact: { email: '', phone: ''},
skills: [],
})
const profileData = ref(defaultProfile())
</script>
まとめ
refは値をコピーしない
初期値のオブジェクトをそのまま渡すと参照先が共有されるだけなので、値を更新すると元のオブジェクトも書き変わってしまう。
スプレッド構文は浅いコピー
ネストされた配列やオブジェクトの参照は共有される。
毎回新しいオブジェクトを生成するのが安全
新しいオブジェクトを生成する関数(factory関数)を定義し、初期化時に呼び出すのが安全




