Skip to content
wannacode

Updating objects and arrays

In short: An object or array in Vue state is changed in place, because Vue watches changes through a proxy; a new array in .value works too, but a plain array without ref does not.

Problem

"Remove" does not remove

The cart has three lines, each with a "Remove" button. Press "Remove" on the bread.

Vue

The console says lines: 2. The screen shows all three lines and the old total. The data changed, the screen does not follow it, and there is no error.

remove puts a new array without the bread into cart. The new value is in the variable. Why does Vue not draw?

The idea

How the library sees a change

With a number it is clear: 1 and 2 are different. An array and an object, though, are references: the variable points to an object in memory. There are two questions. Does the variable point to another object? And did the contents of the same object change? Each library asks in its own way, and here the difference shows in the code.

Vue asks the second question. ref([...]) wraps the array in a proxy: an object that stands in front of the array and sees every read and write. push, splice and line.qty++ go through the proxy. It records the change and tells the template that reads the array to run again. That is why in Vue the array is changed in place, and that is the normal way.

The proxy does not see the variable that points to it, though. In the example let cart = reactive([...]) and then cart = cart.filter(...): the variable now points to a new, plain array. The template reads the old array through the old proxy, and nobody tells it. With const cart = ref([...]) the new array goes into cart.value, and the ref sees that change.

See it

See: the same array or a new one

<script setup>
const cart = ref([bread]);
function addMilk() {
cart.value.push(milk);
}
function addCheese() {
cart.value = [...cart.value, cheese];
}
</script>
Step by step, in words
  1. cart is a ref. Inside it is the array, wrapped in a proxy: an object that stands in front of the array and sees every read and write.
  2. cart.value.push(milk) goes through the proxy. The proxy records: the array changed.
  3. The proxy tells the template, which reads cart, that it has to run again.
  4. The template runs and the screen shows both lines.
  5. This works too: a new array in cart.value. The ref notices the change of .value.
  6. The ref tells the template, and the template runs again with the new array.
  7. The screen shows three lines. In Vue both ways work.
  8. The rule in Vue: change things through the ref. Vue does not see a plain variable or a copy of a number.
Both ways go through the ref: push through the array's proxy, and the new array through .value. Vue misses only what goes around both.

The idea

A change through the proxy

The cart has three actions: add a line, change the quantity of one line, remove a line.

// add
cart.value.push({ ...product, qty: 1 });
// change one line: the line is a proxy too
const line = cart.value.find((l) => l.id === id);
line.qty++;
// remove
const index = cart.value.findIndex((l) => l.id === id);
cart.value.splice(index, 1);
  • Vue watches the objects inside the array too: line from cart.value is a proxy and line.qty++ is seen.
  • The other way works too: a new array in cart.value, for example cart.value = cart.value.filter((l) => l.id !== id). The ref sees the change of .value.
  • A change that goes around the ref is not seen: a plain variable with a new array, or a copy of a number.

Try it

Fix "Remove"

This is the cart from the start. Fix remove so that the line disappears and the total is computed again. The check removes the bread, then the cheese.

Vue

Try it

Find the bug

"+" does not raise the quantity. Which line is to blame?

Pick the line with the bug.

The idea

One line of the array

The customer presses "+" on the bread. The qty of one object has to change, and the object is in an array.

In Vue line.qty++ is the right way: the line from cart.value is a proxy and Vue sees the change. The mistake is to take the number out of the object:

let { qty } = line; // a copy of the number
qty++; // changes the copy

Destructuring and let n = line.qty give a copy. Vue watches the object line, not every variable the number has been in. Write into the object: line.qty++.

More precisely, if you are curious

Deep watching. ref and reactive make the objects inside watched too, when they are first read. shallowRef watches only .value: with it a change inside is not seen, and you put in a new array, as in React.

reactive and destructuring. const { total } = reactive({ total: 0 }) gives a number, not a watched value. toRefs(obj) turns the fields into refs that stay connected to the object.

The new array. cart.value = [...cart.value, line] works and makes a new array, as you would in React. It is useful when the old array must stay the same, for example for "Undo".

structuredClone does not accept a proxy: structuredClone(cart.value) throws an error. First take the plain array with toRaw(cart.value).

Try it

"+" and "−"

Write more and less. "+" raises the quantity of the line by 1, and "−" lowers it by 1. When the quantity is 1, "−" removes the line. The total follows the quantity.

Vue

Exercise

Catalogue and cart

The catalogue has four products with an "Add" button (the name for the screen reader: "Add White bread"). Write add and remove. The first click on "Add" makes a line with quantity 1, and the next ones raise the quantity of the same line instead of making a new one. "Remove" removes the line. When the cart is empty, it says "The cart is empty".

Vue

Test

Check yourself

1. let cart = reactive([...]); and then cart = cart.filter(...). Why does the screen not change?
2. Which one raises the quantity of one line correctly?
3. Does cart.value = [...cart.value, line] work in Vue?
4. const cart = [ ... ] without ref, and then cart.push(line). What happens?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·