Skip to content
wannacode

Derived state

In short: Derived state is a value computed from the state (a count, a total, a filtered list) instead of being stored separately; computed keeps the result until the next change.

Problem

The total falls behind

The cart has white bread and fresh milk. Press "+" on the bread: the count and the total grow. Now remove the milk.

Vue

The line disappears, the count drops to 3, and the total stays €5.20. The component keeps three states: items, count and total. Every click has to change all three, and the remove function changes two. This is the same drift as in the lesson "A list from data", only inside a component. The library draws exactly what you give it. You give it two truths about one cart.

The idea

Compute it, do not keep it

Ask about every piece of state: can I compute it from the other state or from the props? If you can, it is not state. The count is the sum of the quantities in items. The total is the sum of price times quantity. Both are computed.

Such a value is called derived state. You keep only items and compute the rest from them every time the component renders.

const items = ref(START);
const count = computed(() => items.value.reduce((sum, i) => sum + i.qty, 0));
const total = computed(() => items.value.reduce((sum, i) => sum + i.price * i.qty, 0));

<script setup> runs once, when the component is created. That is why the calculation is in a function given to computed. Vue remembers that it reads items and runs it again when items change. In the template you write {{ total }}, without .value.

Now the functions more and remove change only items. There is nothing left for them to forget.

See it

See the computed values and the copy

On the left are the cart and a copy of the total, kept separately. On the right are the three computed values.

<script setup>
const items = ref(START);
const savedTotal = ref(26.9);
const count = computed(() =>
items.value.reduce((s, i) => s + i.qty, 0),
);
const total = computed(() =>
items.value.reduce((s, i) => s + i.price * i.qty, 0),
);
const free = computed(() => total.value >= 30);
function more(item) {
item.qty++;
savedTotal.value += item.price;
}
function remove(item) {
items.value = items.value.filter((i) => i.id !== item.id);
}
</script>
Step by step, in words
  1. On the left is the state: the cart items. Below it is a second state, savedTotal: the same total, kept as a copy.
  2. On the right are count, total and free: computed values. Vue remembers that they read items and computes them again when items change: €26.90, €3.10 left to free delivery.
  3. more(cheese): items change and the three arrows light up. The total is €34.80, delivery is free. more() also updates savedTotal by hand, so the copy is still right.
  4. remove(cheese): items change, and count, total and free follow by themselves: €3.20. Nobody wrote a line for savedTotal in remove().
  5. The copy says €34.80, the cart is €3.20. No error, only a wrong number: the state and its copy have drifted apart.
  6. The rule: keep only what cannot be computed. Whatever follows from the state is computed from it, so there is no copy to forget.

The computed values follow the cart on every change, because they have no memory of their own. The copy follows only as long as somebody remembers to update it. The first function that forgets makes it wrong.

Try it

Compute the count and the total

The cart keeps count and total as separate states, and remove() forgets the total. Remove both states and compute the count and the total from items. The check presses "+" on the bread, then removes the milk and looks at the count and the total.

Vue

Try it

Fill in

The count is computed from the cart. What goes in the blank?

const items = ref(START);
const count = (() => items.value.reduce((sum, i) => sum + i.qty, 0));

The idea

When to keep the result

computed already keeps the result. It is computed when you first read it. After that, as long as the data it read do not change, it returns the remembered value. So two places in the template with {{ total }} compute the total once.

const sorted = computed(() => products.value.toSorted(byPrice));

A function called in the template ({{ totalOf(items) }}) also gives the right total. The difference is that it runs on every render of the component, and computed only when its dependencies change. The function in computed only reads and returns. It does not change state and does not call fetch.

The idea

The filter is computed too

The catalogue has the buttons "All", "Bread" and "Milk". The temptation is to keep two arrays: all products and the visible ones. But the visible ones follow from two other facts: the full list and the chosen category. You keep only the category:

const category = ref('all');
const visible = computed(() =>
category.value === 'all' ? PRODUCTS : PRODUCTS.filter((p) => p.category === category.value),
);

Sorting is the same: you keep "by price" or "by name", and the sorted list is computed. Watch out for sort(): it sorts the array in place, which means it changes the state. toSorted() returns a new array and leaves the old one.

Try it

Only the category

The buttons change the category, but the list always shows everything. Compute the visible products from the category and show "Showing 3 of 5" above the list. The check picks "Milk", then "Bread", then "All".

Vue

The idea

A copy of a prop

The same mistake happens between components. App gives 20% off and passes the new prices to every ProductCard. The card, however, remembers the price it got the first time:

Vue

Press "20% off". The button is pressed, and the bread is still €1.20.

const price = ref(props.product.price) copies the number once, when <script setup> runs. After that props.product is new, but price is a separate box that nobody changes.

The fix is not to copy: show product.price directly. If the card has to show something computed from a prop (the price with VAT, the price per unit), compute it during render with computed(() => props.product.price * …).

Try it

Find the bug

The buyer presses "+": the quantity on the line becomes 3, and the line total stays as for 2. Which line is to blame?

Pick the line with the bug.

More precisely, if you are curious

When the copy is on purpose. Sometimes you want a value from outside only as a start: an edit form starts with the current name, and then the person types. Then the copy is right, but say so in the name: the prop initialName, the state name. Whoever reads the code sees that changes from outside are not followed.

A writable computed. computed({ get, set }) gives a value that can also be assigned: set turns the new value into a change of the real state. It is rarely used, for example for a field bound to part of an object.

computed is lazy. If nobody reads total, the calculation does not run. That is why there are no side effects inside: you do not know when and how many times it will run. For an action on change there is watch, the next module.

The Options API. In the older style the computed values are in the object computed: { total() { … } }. The idea is the same.

The same holds for data from the server. If the server returns the products and you keep "number of products" separately, the two can drift apart after a new load. Keep the response and compute the count.

Exercise

Free delivery

The cart has "−" and "+" buttons on every line ("−" on the last unit removes the line). Below the list show "Items: 4", "Total: €18.20" and "Free delivery in another €11.80", and from €30 (FREE_FROM) only "Delivery is free". Everything is computed from items. The check adds cheese up to €34.00, then goes back under €30 and removes the bread.

Vue

Test

Check yourself

1. The cart keeps items. Which of these should be a separate state?
2. What is the difference between computed and a function called in the template?
3. const price = ref(props.product.price); in ProductCard. The parent passes a new price. What does the card show?
4. The list is filtered by category. What belongs in the state?

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