A Vue 3 `<UserCard>` edits its `user` object prop in place for inline renaming; what does Vue catch, what slips through, and how do you fix it?
answer
- shallow read-only in development
- binding vs nested field
- parent overwrites on re-render
- ref(props.user) is not a copy
- draft copy, computed, or emit
basics
~20 sReassigning a prop is blocked with a warning in development builds, but mutating a nested field such as props.user.name is not caught and silently changes the parent's object. Fix it with a copied local draft, a computed, or an emitted event.
solid answer
~40 sVue protects the **prop binding**, not the object behind it. In a development build, `props.user = x` in `<script setup>` fails because setup receives a shallow read-only props object (`Set operation on key "user" failed: target is readonly`), an Options API `this.user = …` warns `Attempting to mutate prop "user"`, and `v-model="user"` on the prop is a compile error. Even where a write went through, the next parent update would overwrite it. `props.user.name = 'Ann'` is not caught at all: it mutates the parent's object, so the parent changes without knowing why. Fix by editing a **copied draft** (`ref({ ...props.user })`, not `ref(props.user)`, which wraps the same object), deriving with `computed`, or emitting an event so the parent performs the change.
code
vue · 20 lines<script setup>
import { ref } from 'vue'
const props = defineProps({
user: { type: Object, required: true }
})
const emit = defineEmits(['rename'])
// a copy, so typing does not touch the parent's object
const draftName = ref(props.user.name)
function save() {
emit('rename', { id: props.user.id, name: draftName.value })
}
</script>
<template>
<input v-model="draftName" />
<button @click="save">Save</button>
</template>go deeper
Remember that a child must not assign to its props and should emit an event instead.
Explain the shallow read-only guard, the specific warnings, and why nested fields are not protected.
Diagnose a parent changing with no traceable event, and fix it with a copied draft, a computed, or an emit.
Set a team policy on when nested prop mutation is tolerated and how edit flows with drafts and events are standardised.
## What the scenario looks like A `<UserCard>` receives `user` from a list page and offers inline renaming. The quick implementation binds an input to the prop and writes into it. It seems to work, yet the list page changes in ways nobody asked for, or edits vanish when the list refreshes. The cause is how Vue 3 guards props. ## What Vue catches Vue treats props as **owned by the parent**. It guards the top-level binding in several places: | Where the write happens | What Vue does | |---|---| | `props.user = …` in `<script setup>` | in development, setup's props object is shallow read-only: the set fails and warns `Set operation on key "user" failed: target is readonly` | | `this.user = …` in an Options API component | the write is refused; development builds warn `Attempting to mutate prop "user". Props are readonly.` | | `v-model="user"` where `user` is a prop | compile error: v-model cannot be used on a prop, because local prop bindings are not writable | All of these runtime messages are **development-only**. The read-only wrapper on setup's props object is itself a development check, so production code must never depend on it to stop a write. There is also a structural reason not to write: whenever the parent re-renders, Vue refreshes every prop with the parent's latest value. A local assignment would be overwritten on the next update. ## What slips through The guard is **shallow**. `props.user.name = 'Ann'`, `props.user.tags.push('vip')` and `v-model="user.name"` all mutate the object the parent passed, and Vue does not warn. The documentation is explicit that preventing nested mutation would be unreasonably expensive. The effects: - If the parent's object is reactive state, the parent and every other consumer re-render with the new name, with no event anyone can trace. - If the parent re-fetches and replaces the object, the child's edits vanish. - Cancel buttons cannot restore the old value, because there is no copy. ## How to fix it 1. **Edit a local draft.** Copy the prop into local state and commit on save: - `const draft = ref({ ...props.user })` copies the top level. - For nested data, `structuredClone(toRaw(props.user))` makes a deep copy; `structuredClone` cannot clone a reactive proxy, so unwrap it with `toRaw` first. - `ref(props.user)` is **not** a copy: `ref` wraps the same object, so editing the draft still mutates the parent's data. The draft is a snapshot and does not follow later prop updates; resetting it when a different user arrives is a watcher's job. 2. **Derive instead of storing.** If the child only needs a transformed value, use `computed(() => props.user.name.trim())`; it tracks the prop and never writes. 3. **Let the parent mutate.** Emit an event such as `rename` with the new name, or expose the value as a component `v-model`, so the owner makes the change and the data flow stays visible. ## Choosing between them - Form-style editing with save and cancel: a **draft copy** plus an emitted save event. - Display-only transformation: a **computed**. - Tightly coupled parent and child, such as a field component: a **two-way binding** through the component v-model contract. - Mutating the nested object directly is acceptable only when parent and child are deliberately designed as one unit, and that decision should be written down. ## Diagnosing it in an existing codebase - Search for `props.` on the left side of an assignment and for `v-model` bound to prop fields. - Watch for a parent's state changing with no event handler in its template. - In development, look for the readonly warnings above; for nested writes there is no warning, so the Vue DevTools timeline of state changes is the faster lead. ## Why Vue does not guard nested fields Making nested writes impossible would mean wrapping every object a parent passes in a deep read-only proxy for each child, and handing children a different object identity from the one the parent holds. That costs memory and time on every render and breaks identity comparisons between parent and child. Vue therefore guards only what it owns, the prop bindings on the child's props object, and leaves object contents to convention. The practical rule in code review: a child may read anything it receives, and it changes parent data only by asking, through an event or a two-way binding the parent opted into.
- Why does `const draft = ref(props.user)` in a Vue 3 child still change the parent's data when you edit `draft.value.name`?`ref` does not copy an object: it stores it and returns a reactive proxy over the same underlying object. `draft.value.name = 'Ann'` therefore writes into the parent's object. Copy first, with `{ ...props.user }` for a flat object or `structuredClone(toRaw(props.user))` for nested data.
- In Vue 3, why is a local assignment to a prop pointless even if a production build let it through?Props are refreshed from the parent on every parent update. Any value the child wrote is replaced by the parent's current value on the next re-render, so the child's change is lost at an unpredictable moment. The child should keep its own state or ask the parent to change the source.
- Is mutating a nested field of an object prop in Vue 3 ever acceptable?Only when parent and child are tightly coupled by design, such as a private sub-component that exists to edit its parent's object. Even then the flow is invisible from the parent's template, so most teams prefer an emitted event, and the exception should be documented.
saying these in an interview costs you the question
- Thinks Vue deep-freezes object props so nested writes warn
- Believes ref(props.user) creates an independent copy
- Relies on the readonly warning to stop writes in production
- Expects a locally assigned prop to survive the next parent update
- Uses v-model directly on a prop name in the child template