skip to content

A Vue 3 options component uses two mixins that each declare a loading flag, and its spinner vanishes before the data arrives; how do you diagnose and fix it?

level: seniorimportance: should knowfreq 38%

answer

  1. two mixins, one data key
  2. shallow merge, no warning
  3. first request to finish wins
  4. one composable at a time

basics

~20 s

Both mixins' data() return loading, so Vue 3 merges them into one reactive key with no warning; whichever request finishes first sets it false. Fix by giving each concern its own state, ideally a composable per mixin combined with a computed.

solid answer

~40 s

Vue 3 shallow-merges every `data()` result into one object, so two mixins that both return `loading` share a single reactive property, and a same-option clash produces no warning. Each mixin sets `this.loading = true` before its request and `false` after, so the faster request clears the flag while the slower one is still pending. I would confirm it by inspecting the component in Vue Devtools or logging `this.$options.mixins`, where one `loading` key serves two mixins. Renaming the keys stops the bug but leaves the collision risk in place. The durable fix is to extract each mixin into a composable that returns its own `loading` ref, call them from `setup()` in the existing component, derive `const loading = computed(() => usersLoading.value || ordersLoading.value)`, and migrate one mixin at a time behind a test.

code

vue · 28 lines
vue
<script>
import { computed } from 'vue'
import { useUsers } from './useUsers'
import { useOrders } from './useOrders'
import AppSpinner from './AppSpinner.vue'
import OrdersTable from './OrdersTable.vue'

// Before: mixins: [usersMixin, ordersMixin], both returning { loading } from data()
export default {
  components: { AppSpinner, OrdersTable },
  setup() {
    const { users, loading: usersLoading } = useUsers()
    const { orders, loading: ordersLoading } = useOrders()
    const loading = computed(() => usersLoading.value || ordersLoading.value)
    return { users, orders, loading }
  },
  methods: {
    exportCsv() {
      // unchanged options code still reads this.users and this.orders
    }
  }
}
</script>

<template>
  <AppSpinner v-if="loading" />
  <OrdersTable v-else :rows="orders" @export="exportCsv" />
</template>

go deeper

for a junior

Recall that data from mixins and the component ends up in one object, so two mixins cannot each own a property with the same name.

for a middle

Explain the shallow data merge that produces one shared loading key and why no warning fires for a clash inside a single option.

for a senior

Demonstrate the diagnosis path and an incremental migration: test first, one mixin per change, remove the mixin from the component, global mixins last.

for a principal

Weigh a quick rename against a migration budget: decide which mixins are shared widely enough to justify composables now and set a rule against adding new ones.

## The symptom A dashboard component written in the Options API lists `mixins: [usersMixin, ordersMixin]`. Each mixin fetches its own data in `created` and toggles a spinner flag it declared in its own `data()`: - `usersMixin` returns `{ loading: false, users: [] }` and sets `this.loading` around its request. - `ordersMixin` returns `{ loading: false, orders: [] }` and does the same. Users complain that the spinner disappears while the orders table is still empty. ## Why it happens Vue 3 merges mixin options before the first instance is created. For `data`, both functions run and their returned objects are **merged one level deep** into one object, which becomes the instance's reactive state. Two mixins returning `loading` therefore produce **one** property, not two. The timeline is then: 1. `usersMixin`'s `created` runs and sets `loading = true`. 2. `ordersMixin`'s `created` runs and sets `loading = true` again. 3. The users request resolves first and sets `loading = false`. 4. The template hides the spinner, although orders are still pending. Nothing warns you. Vue's development-mode duplicate check only fires when the same key appears in **different** options — a data key that is also a method, a prop or a computed. A clash inside `data` is resolved silently by the merge. ## Diagnosing it in a real codebase - **Look at the instance, not the files.** Vue Devtools shows one `loading` in the component's data; logging `JSON.stringify(this.$data)` shows the same. - **List the sources.** `this.$options.mixins` shows which mixins were merged; remember `extends` and global `app.mixin()` sources too, which are not in that array. - **Search every mixin for the key.** A grep for `loading` across the mixin directory, including nested `mixins` inside mixins, usually finds the second owner. - **Check writers, not just declarers.** A mixin may write `this.loading` without declaring it, relying on another mixin — the implicit coupling that makes these bugs hard to see. ## Fixing it | Option | Effect | Trade-off | |---|---|---| | Rename keys (`usersLoading`, `ordersLoading`) | Bug gone | Next mixin can collide again; every consumer's template must change | | Counter in one shared mixin | One flag, correct | Couples two unrelated fetches through a shared key | | One composable per mixin | Each flag is its own ref | Requires migrating consumers, gradually | The composable route removes the cause. `useUsers()` and `useOrders()` each create and return their own `loading` ref. In the existing options component, a `setup()` function calls both, renames the flags on destructuring, and returns a combined `computed` so the template can keep reading `loading`: - the combined flag is true while **either** request is pending; - each flag's source is visible in the file; - no merge rule is involved. ## Migrating without breaking other consumers The mixins are usually shared by many components, so the change is incremental: 1. **Write a test first** that mounts the component with one request pending and asserts the spinner is still shown. 2. **Extract the composable** from one mixin, and have the mixin itself delegate to it if other components still depend on the mixin. 3. **Remove that mixin from this component** in the same change. Leaving it attached is a trap: a binding returned from `setup()` is read before a data key of the same name on `this`, so the old key would be silently shadowed and harder to reason about. 4. **Repeat per mixin**, leaving global mixins for last because they touch every component. 5. **Delete a mixin** once nothing imports it, instead of keeping it for compatibility. ## Preventing a recurrence Once the bug is fixed, the same shape will reappear wherever mixins remain, so it is worth closing the door: - **Freeze the mixin directory**: agree in review that no new mixin is added and no new key is added to an existing one. - **Keep the regression test** that holds one request pending; it documents the combined-loading contract for the next person. - **Prefer derived state**: a flag computed from each request's own state cannot drift, whereas two writers of one boolean always can. ## What the interviewer is checking That you know the data merge is shallow and silent, can find the second owner of a key quickly, and choose a fix that removes the shared namespace rather than moving the collision to a new name.

  • Why is renaming the two data keys only a partial fix?
    It removes this collision but keeps the shared namespace. Every mixin still writes into the same instance, so the next mixin that declares `usersLoading` collides the same silent way, and readers still cannot tell which mixin owns which key. Composables remove the namespace: each value is a local variable with a visible source.
  • What would change if one mixin declared loading in data and the other declared it as a computed?
    Then the clash spans two options, and Vue 3's development-mode duplicate check warns, with text of the form `Computed property "loading" is already defined in Data.` The bug is at least visible in the console. A clash within `data` alone gets no warning, which is why the two-data-flags case survives to production.

saying these in an interview costs you the question

  • Vue keeps a separate loading property for each mixin that declares it.
  • Vue always warns in development when two mixins declare the same data key.
  • Renaming the keys fixes the underlying problem permanently.
  • Keeping the old mixin attached after adding the composable is harmless.
  • Moving both requests into one mixin is the recommended long-term fix.