253 lines
4.6 KiB
Markdown
Executable File
253 lines
4.6 KiB
Markdown
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.
|
|
|
|
```ts
|
|
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`.
|
|
|
|
```ts
|
|
count.value += 1;
|
|
username.value = 'new name';
|
|
isLoading.value = true;
|
|
```
|
|
|
|
In templates, Vue unwraps refs automatically.
|
|
|
|
```vue
|
|
<template>
|
|
<p>{{ count }}</p>
|
|
</template>
|
|
```
|
|
|
|
You write `count`, not `count.value`, in the template.
|
|
|
|
## Python comparison
|
|
|
|
Python:
|
|
|
|
```python
|
|
count = 0
|
|
count += 1
|
|
```
|
|
|
|
Vue script:
|
|
|
|
```ts
|
|
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
|
|
|
|
```vue
|
|
<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.
|
|
|
|
```ts
|
|
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:
|
|
|
|
```ts
|
|
const firstName = ref('Ada');
|
|
const lastName = ref('Lovelace');
|
|
const fullName = ref('Ada Lovelace');
|
|
```
|
|
|
|
Now you must remember to update `fullName` yourself every time.
|
|
|
|
Better:
|
|
|
|
```ts
|
|
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
|
|
```
|
|
|
|
## Python comparison
|
|
|
|
This is similar to a property.
|
|
|
|
```python
|
|
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:
|
|
|
|
```ts
|
|
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
|
|
|
|
```ts
|
|
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:
|
|
|
|
```ts
|
|
count += 1;
|
|
```
|
|
|
|
Right:
|
|
|
|
```ts
|
|
count.value += 1;
|
|
```
|
|
|
|
### using `computed` for side effects
|
|
|
|
Bad:
|
|
|
|
```ts
|
|
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:
|
|
|
|
```ts
|
|
const selectedFirst = ref('2026-01-01');
|
|
const selectedSecond = ref('2026-01-10');
|
|
const isValid = ref(true);
|
|
```
|
|
|
|
Better:
|
|
|
|
```ts
|
|
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:
|
|
|
|
```ts
|
|
const firstName = ref('');
|
|
const lastName = ref('');
|
|
const fullName = computed(() => `${firstName.value} ${lastName.value}`.trim());
|
|
const isSubmitDisabled = computed(() => !firstName.value.trim());
|
|
```
|
|
|
|
## Related Notes
|
|
|
|
- [[00-Vue for Python Developers]]
|
|
- [[02-Templates, v-model, and Events]]
|
|
- [[04-FlexibilityTable Filter Flow]]
|