Skip to content

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: celsius is 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.

Released under the MIT License.