Plain getter vs computed()
One class, one dependency (celsius), two derivations: fahrenheit is a plain getter, status is a computed(). The run counters under each value show exactly when each body executes.
What to notice
- Drag the slider:
celsiusis a dependency of both, so both bodies run — memoization never skips a real dependency change. - Click re-render: the plain getter re-derives (that is its deal — zero bytes, re-run per render) while the computed body stays frozen. That skip is what its ~300 bytes per instance buy.
- The run counters are plain fields — incrementing them inside getter bodies is side-effect-free for the reactive graph.
The source
ts
// Thermo.ts — plain getter vs computed(), side by side.
import { computed, ref } from 'vue';
import { Reactive } from '../../ivue';
class $Thermo {
// Run counters are plain fields: incrementing them inside getter bodies is
// side-effect-free for the graph (a plain-field write triggers nothing).
fahrenheitRuns = 0;
statusRuns = 0;
get celsius() {
return ref(21);
}
// Plain getter: re-derives on EVERY render — even unrelated ones.
get fahrenheit() {
this.fahrenheitRuns++;
return Math.round((this.celsius.value * 9) / 5 + 32);
}
// computed(): memoized — its body runs only when celsius actually changed.
get status() {
return computed(() => this.deriveStatus());
}
deriveStatus() {
this.statusRuns++;
const celsius = this.celsius.value;
if (celsius < 10) return 'Cold';
if (celsius < 18) return 'Cool';
if (celsius < 26) return 'Comfortable';
return 'Warm';
}
}
export namespace Thermo {
export const $Class = $Thermo; // raw — children `extends` this
export let Class = Reactive($Class); // reactive — you `new` this
export type Instance = typeof Class.Instance; // defineExpose type & reactive() interop
}vue
<script setup lang="ts">
import { DerivedExample } from './DerivedExample';
const view = new DerivedExample.Class();
const thermo = view.thermo;
// the state destructure — the view's refs AND the model's refs, grouped
const {
// state refs
ticks,
fahrenheitRunsShown,
statusRunsShown,
} = view;
const {
// state refs
celsius,
// computed refs
status,
} = thermo;
</script>
<template>
<div class="pane">
<p class="note">
Drag the slider: celsius is a dependency of BOTH, so both bodies run.
Now click re-render: the plain getter re-derives (zero bytes, re-run per
render) while the computed body stays frozen — that skip is what its
~300 bytes buy.
</p>
<div class="vals">
<div>
<div class="k">celsius</div>
<div class="n">{{ celsius }}°</div>
</div>
<div>
<div class="k">fahrenheit · plain getter</div>
<div class="n grad">{{ thermo.fahrenheit }}°</div>
<div class="mono">body ran {{ fahrenheitRunsShown }}×</div>
</div>
<div>
<div class="k">status · computed()</div>
<div class="n">{{ status }}</div>
<div class="mono">body ran {{ statusRunsShown }}×</div>
</div>
</div>
<div class="row">
<input
class="slider"
type="range"
min="-5"
max="35"
v-model.number="celsius"
aria-label="celsius"
/>
<button class="btn" type="button" @click="view.reRender()">
re-render ({{ ticks }})
</button>
</div>
</div>
</template>
<style scoped src="../example-pane.css"></style>Open in StackBlitz ⚡ — the playground boots with this example's route and file active.