In Vue 3, how does `ref(state.foo)` differ from `toRef(state, 'foo')`, and when would you use `toRef(() => props.foo)` instead?
answer
- copy versus link
- two-way property ref
- works for missing keys
- getter form is readonly, 3.3+
basics
~20 sref(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 linesimport { 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 // 6go deeper
Recall that ref(state.foo) copies the value, while toRef(state, 'foo') stays linked to the property in both directions.
Explain the property ref's read and write path, the default argument and optional keys, and what the getter form adds since 3.3.
Pick copy, link, getter ref or computed on purpose, and flag prop writes through toRef or seeded refs expected to follow props.
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.