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());