Skip to the solution
MediumFrontend9 min read

What do toRef and toRefs do?

346 views

The problem

Question presented to candidate: "What do toRef and toRefs do in Vue 3, and what problem do they solve? Show me where you would use each, and what happens with a key that does not exist yet."

What a strong answer should cover:

  • toRef(obj, 'key') creates a ref that is a two-way link to one property of a reactive object: reading .value reads obj.key, writing .value writes it.
  • toRefs(obj) does that for every property present at call time, returning a plain object of refs that can be destructured without losing reactivity.
  • Main uses: destructuring props in a plain setup(props), returning reactive state from a composable so callers can destructure, and passing one property to a function that expects a ref.
  • toRefs does not pick up keys added later; toRef works for a missing key and can take a default value (3.2.25+).
  • Since 3.3, toRef(() => getter) makes a readonly ref and toValue() normalises a value, ref or getter.
  • Limits: toRefs on a plain object warns; refs from props or readonly stay read-only. storeToRefs is the Pinia counterpart.

Clarifying questions expected:

  • "Are we in <script setup>, where props destructure is compiled since 3.5, or a plain setup(props) function?"
  • "Is this about props, a composable's return value, or a store?"

Code / implementation expected: Yes β€” a child that destructures props both ways, and a composable that returns toRefs(state).

  • toref
  • torefs

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 Β· 8 sections

Read the code

VueProps destructured with and without toRefs, a composable returning toRefs, and toRef
<script setup>
import { ref, reactive, toRef, toRefs, computed, defineComponent, h } from "vue";

// A child written with a plain setup(props): destructuring props here is NOT compiled away.
const Greeting = defineComponent({
  props: { name: String },
  setup(props) {
    const { name: copied } = props;          // a plain string, frozen at setup time
    const { name: linked } = toRefs(props);  // a ref that follows the prop
    const shout = computed(() => linked.value.toUpperCase());
    return () => h("p", { id: "child" }, `copied: ${copied} | toRefs: ${linked.value} | computed: ${shout.value}`);
  },
});

// A composable that keeps reactive state inside but hands out refs,
// so callers can destructure the result safely.
function useCart() {
  const state = reactive({ items: 0, total: 0 });
  function add(price) { state.items++; state.total += price; }
  return { ...toRefs(state), add };
}
const { items, total, add } = useCart();

// toRef: one property of a reactive object, as a two-way ref.
const settings = reactive({ theme: "light" });
const theme = toRef(settings, "theme");

const who = ref("Ada");
</script>

<template>
  <Greeting :name="who" />
  <button id="rename" @click="who = who === 'Ada' ? 'Grace' : 'Ada'">Rename</button>
  <p id="cart">{{ items }} items, total {{ total }}</p>
  <button id="add" @click="add(5)">Add 5</button>
  <p id="theme">theme ref: {{ theme }} | settings.theme: {{ settings.theme }}</p>
  <button id="toggle" @click="theme = theme === 'light' ? 'dark' : 'light'">Toggle via ref</button>
</template>
Ten toRef / toRefs / toValue behaviours probed in Node
import { reactive, ref, toRef, toRefs, toValue, isRef, isReadonly, readonly, watch, nextTick, unref } from "vue";

const state = reactive({ name: "Ada", age: 36 });

console.log("1. toRef is a two-way link to ONE property");
const age = toRef(state, "age");
age.value++;
console.log(`   after age.value++: state.age=${state.age}`);
state.age = 40;
console.log(`   after state.age=40: age.value=${age.value}`);

console.log("2. toRefs converts EVERY property, so destructuring stays live");
const { name } = toRefs(state);
name.value = "Grace";
console.log(`   state.name=${state.name} | isRef(name)=${isRef(name)}`);

console.log("3. toRefs only sees keys that exist when it is called");
const all = toRefs(state);
state.email = "g@h.io";
console.log(`   keys from toRefs: ${Object.keys(all)} | email in result: ${"email" in all}`);
const email = toRef(state, "email");
console.log(`   toRef for that key works: ${email.value}`);
const missing = toRef(state, "phone");
missing.value = "555";
console.log(`   toRef on a key that did not exist yet, then written: state.phone=${state.phone}`);

console.log("4. toRef with a default value");
const nick = toRef(state, "nick", "anon");
console.log(`   nick.value=${nick.value} (state.nick is ${state.nick})`);

console.log("5. toRef(getter) (3.3+) makes a readonly ref");
const label = toRef(() => `${state.name} (${state.age})`);
console.log(`   label.value=${label.value} | isRef=${isRef(label)} | isReadonly=${isReadonly(label)}`);
try { label.value = "x"; } catch (e) { console.log(`   write throws ${e.constructor.name}: ${e.message}`); }

console.log("6. toRef on a value or ref");
const plain = toRef(5);
const r = ref(1);
console.log(`   toRef(5).value=${plain.value} | toRef(r) === r: ${toRef(r) === r}`);

console.log("7. toRefs on a plain object");
const p = toRefs({ a: 1 });
console.log(`   isRef(p.a)=${isRef(p.a)}`);

console.log("8. toRefs on a readonly proxy keeps it readonly");
const ro = toRefs(readonly(reactive({ x: 1 })));
ro.x.value = 2;
console.log(`   x after write: ${ro.x.value}`);

console.log("9. watching a toRef");
watch(toRef(state, "age"), (v, o) => console.log(`   watch toRef: ${o} -> ${v}`));
state.age = 41; await nextTick();

console.log("10. toValue / unref");
console.log(`   toValue(ref)=${toValue(r)} toValue(getter)=${toValue(() => 7)} unref(getter) is a function: ${typeof unref(() => 7) === "function"}`);

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 63 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

watch vs watchEffect β€” flush timing and the new watch options in 3.5?

Continue