Files
it-know-how/vue/01-Refs, Computed, and Reactivity.md
2026-03-27 12:58:01 +01:00

4.6 KiB
Executable File

Refs, Computed, and Reactivity

This note explains the most important Vue state concepts for a Python developer.

ref: reactive storage for one value

A ref wraps a value so Vue can track it.

import { ref } from 'vue';

const count = ref(0);
const username = ref('mathias');
const isLoading = ref(false);

In script code, you read and write using .value.

count.value += 1;
username.value = 'new name';
isLoading.value = true;

In templates, Vue unwraps refs automatically.

<template>
  <p>{{ count }}</p>
</template>

You write count, not count.value, in the template.

Python comparison

Python:

count = 0
count += 1

Vue script:

const count = ref(0);
count.value += 1;

The extra .value exists because count is a reactive wrapper object, not the raw number.

Example: loading state

<template>
  <button :disabled="isLoading" @click="save">
    {{ isLoading ? 'Saving...' : 'Save' }}
  </button>
</template>

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

const isLoading = ref(false);

async function save() {
  isLoading.value = true;
  try {
    await fakeRequest();
  } finally {
    isLoading.value = false;
  }
}

function fakeRequest() {
  return new Promise((resolve) => setTimeout(resolve, 1000));
}
</script>

The button label and disabled state both react to isLoading.

computed: derived state

A computed value is calculated from other reactive values.

import { ref, computed } from 'vue';

const firstName = ref('Ada');
const lastName = ref('Lovelace');

const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`;
});

Use computed when a value can be derived instead of stored manually.

Bad pattern:

const firstName = ref('Ada');
const lastName = ref('Lovelace');
const fullName = ref('Ada Lovelace');

Now you must remember to update fullName yourself every time.

Better:

const fullName = computed(() => `${firstName.value} ${lastName.value}`);

Python comparison

This is similar to a property.

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    @property
    def full_name(self):
        return f"{self.first_name} {self.last_name}"

Vue computed plays a similar role.

Real example from your app

In FlexibilityTable, this pattern appears:

const hasValidPeriodRange = computed(() => {
  if (!selectedPeriodStartFrom.value || !selectedPeriodStartUntil.value) {
    return true;
  }

  return new Date(selectedPeriodStartFrom.value).getTime() <= new Date(selectedPeriodStartUntil.value).getTime();
});

This means:

  • if one boundary is missing, the range is treated as valid
  • if both are set, start must be before or equal to end

The component does not store hasValidPeriodRange manually. It derives it from the two selected dates.

Another example: filtered list

const searchText = ref('');
const users = ref(['Alice', 'Bob', 'Charlie']);

const filteredUsers = computed(() => {
  return users.value.filter((user) =>
    user.toLowerCase().includes(searchText.value.toLowerCase()),
  );
});

If searchText changes, filteredUsers updates automatically.

Rule of Thumb

Use ref when:

  • the value changes over time
  • the UI should react to that change
  • the value is primary state

Use computed when:

  • the value is calculated from other reactive values
  • you do not want to duplicate state

Common Beginner Mistakes

forgetting .value in script

Wrong:

count += 1;

Right:

count.value += 1;

using computed for side effects

Bad:

const result = computed(() => {
  console.log('side effect');
  return count.value * 2;
});

A computed should mainly calculate and return a value.

storing what can be derived

Bad:

const selectedFirst = ref('2026-01-01');
const selectedSecond = ref('2026-01-10');
const isValid = ref(true);

Better:

const isValid = computed(() => selectedFirst.value <= selectedSecond.value);

Tiny Exercise

What should be ref and what should be computed?

Scenario:

  • user types first name
  • user types last name
  • screen shows full name
  • submit button disabled when first name is empty

Answer:

const firstName = ref('');
const lastName = ref('');
const fullName = computed(() => `${firstName.value} ${lastName.value}`.trim());
const isSubmitDisabled = computed(() => !firstName.value.trim());