Skip to the solution
MediumFrontend9 min read

Can a computed property be writable?

833 views

The problem

Question presented to candidate: "Computed properties are usually read-only. Can you make one writable in Vue 3? Show me the syntax, explain what actually happens on assignment, and give me a case where you would really use it."

What a strong answer should cover:

  • Yes: pass an object with get and set to computed({ get, set }); it returns a WritableComputedRef.
  • The setter stores nothing. It must write back to the source state; the next read re-runs the getter, so the getter always wins.
  • Writing to a getter-only computed logs the dev warning Write operation failed: computed value is readonly and is ignored.
  • Real uses: v-model on a prop (get from the prop, set by emitting update:modelValue), two views of one source (Celsius and Fahrenheit), parsing and clamping input, and binding v-model to store state.
  • Since Vue 3.4 defineModel() covers the prop-plus-emit case in <script setup>; the getter also receives the previous value (3.4), and getter and setter types may differ (3.5).
  • Keep setters synchronous and side-effect free apart from writing the source.

Clarifying questions expected:

  • "Is this for a component's own state, or to support v-model on a child?"
  • "Which Vue version? defineModel changes the answer from 3.4 on."

Code / implementation expected: Yes — a get/set computed bound with v-model, ideally a child component that supports v-model through one.

  • computed
  • writable

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read · 9 sections

Read the code

VueCelsius/Fahrenheit and a v-model Toggle built on writable computeds
<script setup>
import { ref, computed, defineComponent, h } from "vue";

// 1. One source of truth (celsius), two editable views of it.
const celsius = ref(20);
const fahrenheit = computed({
  get: () => Math.round((celsius.value * 9) / 5 + 32),
  set: (f) => {
    console.log("setter got", f);
    celsius.value = Math.round(((f - 32) * 5) / 9); // write back to the source
  },
});

// 2. The classic use: a child that supports v-model without mutating its prop.
const Toggle = defineComponent({
  props: { modelValue: Boolean },
  emits: ["update:modelValue"],
  setup(props, { emit }) {
    const on = computed({
      get: () => props.modelValue,            // read from the parent
      set: (v) => emit("update:modelValue", v), // ask the parent to change it
    });
    return () => h("button", { id: "toggle", onClick: () => (on.value = !on.value) }, on.value ? "ON" : "OFF");
  },
});
const darkMode = ref(false);
</script>

<template>
  <label>C <input id="c" type="number" v-model.number="celsius" /></label>
  <label>F <input id="f" type="number" v-model.number="fahrenheit" /></label>
  <p id="temp">{{ celsius }} C = {{ fahrenheit }} F</p>
  <Toggle v-model="darkMode" />
  <p id="dark">parent darkMode: {{ darkMode }}</p>
</template>
What assignment really does: setter, clamping, previous value, readonly warning
import { ref, computed, watch } from "vue";

// 1. Basic writable computed: the setter writes back to the SOURCE refs
const first = ref("Ada");
const last = ref("Lovelace");
let getterRuns = 0;
const fullName = computed({
  get: () => { getterRuns++; return `${first.value} ${last.value}`; },
  set: (v) => {
    console.log(`   setter called with "${v}"`);
    [first.value, last.value = ""] = v.split(" ");
  },
});
console.log("1.", fullName.value);
fullName.value = "Grace Hopper";
console.log("   first:", first.value, "| last:", last.value, "| fullName:", fullName.value, "| getter runs:", getterRuns);

// 2. The setter does not store anything: if it ignores the value, the getter wins
const clamped = ref(5);
const percent = computed({
  get: () => clamped.value,
  set: (v) => { clamped.value = Math.min(100, Math.max(0, v)); },
});
percent.value = 250;
console.log("2. wrote 250, read back:", percent.value);

// 3. A setter with no effect on the sources: value does not change
const fixed = computed({ get: () => 42, set: (v) => console.log("   ignored", v) });
fixed.value = 7;
console.log("3. after writing 7:", fixed.value);

// 4. Since 3.4 the getter receives the previous value
const count = ref(2);
const lastEven = computed((previous) => (count.value % 2 === 0 ? count.value : previous));
const seen = [];
for (const v of [2, 3, 4, 5, 7]) { count.value = v; seen.push(`${v}->${lastEven.value}`); }
console.log("4. previous-value getter:", seen.join(", "));

// 5. Getter-only computed is readonly
const doubled = computed(() => count.value * 2);
doubled.value = 1;

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 35 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

How do you lazy-load routes and components in Vue?

Continue