skip to content

In Vue 3's Options API, when a mixin and its component both define data, methods, watch and created, how is each merged?

level: middleimportance: should knowfreq 48%

answer

  1. one strategy per option
  2. hooks queue, objects overwrite
  3. shallow top-level data merge
  4. global, extends, mixins, then own

basics

~20 s

Vue 3 merges each option by its own strategy: data() results are shallow-merged, methods are object-merged, and created hooks and watch handlers are queued so all run, mixins first. On a name clash the component's own value wins.

solid answer

~40 s

Vue 3 resolves a component's final options once, merging sources in a fixed order: global mixins from `app.mixin()`, then `extends`, then each entry of `mixins` in array order, then the component itself. Each option has its own strategy. Both `data()` functions run and their results are shallow-merged, so on a top-level key clash the component wins and a nested object is replaced, not combined. `methods`, `computed`, `components`, `directives` and `inject` are object-merged with the later source winning. Lifecycle hooks such as `created` are concatenated, so every hook runs, mixin hooks before the component's. `watch` is merged per key, so every handler for the same key fires. A clash inside one option is silent; only a clash across options, such as a data key that is also a method, gets a dev-mode warning.

code

vue · 37 lines
vue
<script>
const listMixin = {
  data() {
    return { loading: false, filters: { q: '' } }
  },
  methods: {
    refresh() { console.log('mixin refresh') }
  },
  created() { console.log('mixin created') }
}

export default {
  mixins: [listMixin],
  data() {
    return { filters: { page: 1 } }
  },
  methods: {
    refresh() { console.log('component refresh') }
  },
  created() {
    console.log('component created')
    console.log(JSON.stringify(this.$data))
    this.refresh()
  }
}
</script>

<template>
  <p>Page {{ filters.page }}</p>
</template>

<!-- Console order:
mixin created
component created
{"loading":false,"filters":{"page":1}}
component refresh
-->

go deeper

for a junior

Recall that a mixin's hooks and the component's hooks both run, mixin first, and that the component's own data keys and methods win a name clash.

for a middle

Explain the per-option strategies — shallow data merge, object merge for methods and computed, arrays for hooks and watch — and the fold order from global mixins to the component.

for a senior

Show that same-option clashes are silent, so mixin bugs appear as wrong behaviour, and describe auditing a component by inspecting its resolved this.$options.

for a principal

Frame the merge model as the real cost of mixins: precedence rules that make a property's owner unknowable from the file, which is the argument for moving shared logic into composables.

## What merging means here A **mixin** in Vue 3 is a plain options object — the same shape as a component definition — listed in a component's `mixins` array. Vue does not copy it in blindly. Before the first instance of a component is created, Vue builds one **resolved options object** by folding every source into it, and each option key is combined by its own **merge strategy**. The result is what the instance actually runs, and it is visible as `this.$options`. The resolution is done **once per component definition** and cached per application, because it depends only on the definitions, never on instance state. ## The order sources are folded in The order matters because, for object-like options, the source folded in later wins: 1. **Global mixins** registered with `app.mixin()`, in registration order. 2. The component's **`extends`** base, treated as though it were the first mixin. 3. Each entry of **`mixins`**, in array order (each mixin's own `extends` and `mixins` are folded in first, recursively). 4. The **component's own options**, last. So the component's own value beats every mixin, and of two mixins the later one in the array beats the earlier. ## The strategy for each option | Option | Strategy | Result on a clash | |---|---|---| | `data`, `provide` | Both functions run; their returned objects are merged one level deep | Component's top-level key wins; a nested object is replaced whole | | `methods`, `computed`, `components`, `directives`, `inject` | Object merge into a fresh object | Later source wins, silently | | Lifecycle hooks (`created`, `mounted`, `unmounted`, …) | Concatenated into an array | All run: global mixins, extends, mixins, then the component | | `watch` | Merged key by key into arrays | Every handler for that key runs | | `props`, `emits` | Two arrays are unioned; otherwise normalised and object-merged | Later definition of the same prop wins | | `setup`, `expose` | Not merged | Only the component's own `setup()` runs; `expose` in a mixin is ignored with a dev warning | | Any other key | `app.config.optionMergeStrategies[key]` if registered, else overwrite | Later source wins | Two details are easy to miss: - The `data` merge is **shallow**. If a mixin returns `filters: { q: '' }` and the component returns `filters: { page: 1 }`, the instance ends up with `{ page: 1 }` — the mixin's `q` is gone. - Hook arrays are **de-duplicated by function reference**, so the same hook function reaching a component through two paths runs once, not twice. ## What a collision looks like The merge rules decide who wins, but they rarely tell you a clash happened: - **Same key, same option** — two mixins both declaring `methods.refresh`, or both returning `loading` from `data()` — is resolved silently by the later-wins rule. No warning, in development or production. - **Same key, different options** — a `data` key that is also a method, a prop or a computed — is caught in development by a duplicate-property check that prints a warning such as `Data property "refresh" is already defined in Methods.` - **Hooks and watchers never collide**; they accumulate, which is its own trap when two mixins both start a timer in `mounted`. This is why mixin bugs tend to surface as wrong behaviour rather than as errors. ## Custom options Plugins sometimes invent their own component options. Such a key has no built-in strategy, so by default the later source simply overwrites the earlier one. `app.config.optionMergeStrategies` lets you register a function `(parent, child) => merged` for a custom key; the resolved value is then read from `this.$options`. Vue looks up its **internal strategy first**, so this hook cannot change how `data`, `methods`, hooks or `watch` merge — it only affects keys Vue does not already know. ## Why an interviewer asks this The question tests whether you can predict what a legacy component actually does from its definition: which `created` runs first, why a nested data default vanished, why one of two same-named methods is never called. It also sets up the follow-on question of why composables — where every value is an explicit variable — replaced mixins as Vue 3's recommended reuse mechanism.

  • Can you change how Vue 3 merges methods by assigning app.config.optionMergeStrategies.methods?
    No. During resolution Vue looks up its internal strategy for a key first and only falls back to `app.config.optionMergeStrategies` when no internal strategy exists. The built-in rules for `data`, `methods`, `computed`, hooks, `watch`, `props`, `emits`, `provide` and `inject` therefore cannot be replaced there. The config object is for custom options a plugin reads back from `this.$options`.
  • Is the merge redone every time a component instance is created?
    No. Vue 3 resolves the merged options once per component definition and caches them per application, because the result depends only on the definitions. Every instance reads the cached object, exposed as `this.$options`. That is also why nothing about the merge can depend on props or instance data.
  • What happens if the same mixin is applied twice, globally with app.mixin() and locally?
    Calling `app.mixin()` twice with the same object logs the dev warning `Mixin has already been applied to target app` and adds it once. When the same hook function reaches a component through both a global and a local path, the hook array is de-duplicated by reference, so it runs once. Object options are simply overwritten by the identical value.

saying these in an interview costs you the question

  • The component's created hook replaces the mixin's created hook of the same name.
  • Mixin data is deep-merged, so nested objects combine key by key.
  • Vue warns whenever two mixins define a method with the same name.
  • Mixin hooks run after the component's own hooks.
  • Every option follows the same last-one-wins rule, hooks included.