skip to content

In Vue 3, how does `ref(state.foo)` differ from `toRef(state, 'foo')`, and when would you use `toRef(() => props.foo)` instead?

level: middleimportance: should knowfreq 38%

answer

  1. copy versus link
  2. two-way property ref
  3. works for missing keys
  4. getter form is readonly, 3.3+

basics

~20 s

ref(state.foo) is an independent ref seeded with the current value. toRef(state, 'foo') is a two-way link to that property, even before it exists. toRef(() => props.foo), since 3.3, is a readonly ref calling the getter on each read.

solid answer

~40 s

`ref(state.foo)` receives a plain value, so it owns its own copy: later changes to `state.foo` do not reach it and writes to it do not reach `state`. `toRef(state, 'foo')` returns a **property ref**: reading `.value` reads `state.foo` through the proxy, writing `.value` writes it back, and it works for optional keys that do not exist yet (which `toRefs()` cannot cover). For props, writing such a ref counts as mutating the prop, which is not allowed (development builds refuse it with a readonly warning). The **getter form**, `toRef(() => props.foo)` (Vue 3.3+), creates a readonly ref that calls the getter on every `.value` access; the docs recommend it for handing a prop to a composable that expects a ref. Unlike `computed`, it does not cache, so it suits cheap getters.

code

ts · 21 lines
ts
import { reactive, ref, toRef } from 'vue'

const state = reactive<{ foo: number; bar?: string }>({ foo: 1 })

const copy = ref(state.foo)        // seeded with 1, independent
const link = toRef(state, 'foo')   // two-way link to state.foo
const opt = toRef(state, 'bar', 'n/a') // key does not exist yet

state.foo = 2
copy.value // 1
link.value // 2

link.value = 3
state.foo  // 3

opt.value       // 'n/a' (default while state.bar is undefined)
state.bar = 'x'
opt.value       // 'x'

const doubled = toRef(() => state.foo * 2) // readonly, re-evaluates on each read
doubled.value   // 6

go deeper

for a junior

Recall that ref(state.foo) copies the value, while toRef(state, 'foo') stays linked to the property in both directions.

for a middle

Explain the property ref's read and write path, the default argument and optional keys, and what the getter form adds since 3.3.

for a senior

Pick copy, link, getter ref or computed on purpose, and flag prop writes through toRef or seeded refs expected to follow props.

for a principal

Standardise how components hand props to composables - getters, getter refs or MaybeRefOrGetter - so the codebase has one idiom.

## Three ways to get a ref for one value | Expression | Linked to the source? | Writable? | Caches? | |---|---|---|---| | `ref(state.foo)` | no, it copies the value once | yes, but only its own copy | not applicable | | `toRef(state, 'foo')` | yes, reads and writes `state.foo` | yes, writes go to the source | no, reads the source each time | | `toRef(() => props.foo)` | yes, calls the getter on every `.value` | no, readonly | no | | `computed(() => props.foo)` | yes, tracked getter | no (unless given a setter) | yes, recomputes only after a change | ## `ref(state.foo)`: a copy `ref()` receives whatever the expression `state.foo` evaluated to. If that is a number or string, the new ref holds its own value and its own dependency list. The docs call this out next to `toRef()`: the ref is **not** synced with `state.foo`, because `ref()` receives a plain number. It is the right tool for local state *seeded* from something else - an editable draft of a prop, for example. ## `toRef(state, 'foo')`: a property ref The object-property signature returns a ref that stores the source object and the key, and nothing else: - `.value` reads `state.foo` - through the proxy, so the read is tracked when an effect is running; - assigning `.value` writes `state.foo`, which triggers the property's subscribers; - a third argument supplies a default returned when the property is `undefined`; - it works for a key that is **not present yet**, which is why it complements `toRefs()` (that one iterates only existing keys). With props, the source is readonly. Assigning to `toRef(props, 'foo').value` is equivalent to mutating the prop, which the docs say is not allowed - development builds refuse it with a readonly warning; when a child needs to change a prop's value, use a `computed` with `get` and `set` that emits an update instead, or `defineModel()`. ## `toRef(() => x)`: the getter form (3.3+) Passing a function to `toRef()` normalises it into a **readonly ref** whose `.value` calls the getter on each access. It exists mostly for APIs that expect a ref: ```ts const props = defineProps<{ userId: number }>() useUserPanel(toRef(() => props.userId)) ``` Because it calls the getter every time, any reactive reads inside the getter are tracked by whichever effect reads `.value`. It is cheap to create and has no cache to invalidate, but an expensive getter would re-run on every read - use `computed` for that. `toRef()` has two more normalisation cases: an existing ref is returned as-is, and any other non-function value becomes `ref(value)`. ## Choosing between them 1. Need an **independent** local copy you will edit? `ref(value)`. 2. Need a **two-way link** to one property of reactive state, possibly optional? `toRef(state, 'key')`. 3. Need a **ref-shaped, readonly view** of a prop or expression for an API expecting a ref? `toRef(() => expr)`. 4. Need a **derived value** that is costly to compute? `computed()`. 5. The consuming function accepts `MaybeRefOrGetter`? Pass the plain getter `() => expr` and skip the wrapper. ## Common mistakes - Using `ref(props.foo)` and expecting it to follow the prop; it is a one-time seed. - Writing to `toRef(props, 'foo')` to change a prop locally. - Replacing a cheap getter ref with `computed` everywhere "for caching" - caching a single property read adds bookkeeping without saving work. ## How `toRefs()` fits in `toRefs(state)` is simply `toRef(state, key)` applied to every key present at the moment of the call, collected into a plain object. That is why the two behave identically for existing keys, and why only `toRef()` helps with optional keys. Neither copies values: every ref they return is a view onto the source property. ## The answer in three sentences `ref(state.foo)` is a copy seeded once. `toRef(state, 'foo')` is a two-way link to one property, which also works for keys that do not exist yet. `toRef(() => props.foo)` is a readonly, uncached ref over a getter, the idiomatic way since 3.3 to give a composable a ref-shaped view of a prop.

  • In Vue 3, when is `computed(() => props.foo)` a better choice than `toRef(() => props.foo)`?
    When the getter does real work. `computed` caches its result and recomputes only after a tracked dependency changes, while the getter ref calls its function on every `.value` read. For a plain property read the two behave the same and the getter ref is lighter.
  • Why can `toRef(state, 'bar')` cover an optional key that `toRefs(state)` misses?
    `toRefs()` builds refs by iterating the keys present when it is called, so a key added later has no ref. `toRef(state, 'bar')` stores the object and the key name and reads `state.bar` on each access, so it works before and after the key exists.

saying these in an interview costs you the question

  • ref(state.foo) stays in sync with state.foo.
  • toRef(state, 'foo') throws if foo does not exist yet.
  • Assigning toRef(props, 'foo').value is a safe way to change a prop locally.
  • toRef(() => x) caches its value like computed().
  • toRef(() => x) creates a writable ref whose writes update x.