skip to content

In a Vue 3 Options API component, why must data be a function returning an object, and what does Vue make reactive?

level: juniorimportance: must knowfreq 62%

answer

  1. one call per instance
  2. fresh object each time
  3. wrapped in reactive(), proxied on this
  4. declare keys up front; _ and $ skipped

basics

~20 s

Vue 3 calls data() once per component instance, so each instance gets its own state object. The result is wrapped with reactive() and its top-level keys are proxied on this; keys added later, or starting with _ or $, are not.

solid answer

~50 s

`data()` is a factory: Vue 3 calls it once per instance while the instance is being created, so two copies of the same component never share state. The returned plain object is made reactive with `reactive()`, exposed as `this.$data`, and each top-level key is proxied so `this.count` means `this.$data.count`. Only keys present in that returned object are reactive through `this`, so declare every key up front, using `null` as a placeholder if the value arrives later; a property assigned to `this` later is stored but cannot trigger updates. Keys starting with `_` or `$` are not proxied and must be read as `this.$data._key`. `data()` cannot be async — Vue warns if it returns a Promise — and a non-function `data` option gets a dev warning too. Inside `data()`, props, injected values and methods are already available.

code

js · 21 lines
js
const sharedFilters = { tag: 'all' }

export default {
  props: { initialQuery: { type: String, default: '' } },
  data() {
    return {
      query: this.initialQuery, // props are readable here
      results: [],
      selected: null,           // placeholder: declared up front
      _requestId: 0,            // reactive, but not proxied on this
      filters: sharedFilters    // bug: every instance shares this object
    }
  },
  methods: {
    select(item) {
      this.selected = item        // reactive: declared in data()
      this.lastPicked = item      // stored on this, but never triggers a re-render
      this.$data._requestId++     // _-prefixed keys go through $data
    }
  }
}

go deeper

for a junior

Recall that data() runs once per instance to give each one its own state, and that only keys it returns are reactive through this.

for a middle

Explain the reactive() wrapping, $data proxying, the _ and $ prefix exception, and what data() can already read: props, inject and methods, not computed.

for a senior

Spot the shared-object bug in data(), the late-added property that never updates the view, and the fetch that belongs in a hook rather than an async data().

for a principal

Use the data() contract as a review checklist for legacy options code: declared keys, no shared module state, no async initialisation hidden in the factory.

## What the data option is In Vue 3's **Options API**, a component is described by an object with keys such as `data`, `methods`, `computed` and `watch`. The `data` option declares the component's **local reactive state**. It must be a **function that returns a plain object**: ```js export default { data() { return { query: '', results: [], selected: null } } } ``` ## Why a function One component definition is used to create many **instances** — every `<SearchBox>` on a page is a separate instance built from the same options object. If state were a single object stored on the definition, every instance would read and write the same object: typing in one search box would change all of them. Because `data` is a function, Vue 3 **calls it once per instance**, during instance creation, and each call returns a brand-new object. The instances share code but not state. In development, a `data` option that is not a function produces the warning `The data option must be a function. Plain object usage is no longer supported.` ## What Vue does with the returned object 1. It checks the result is an object (otherwise it warns `data() should return an object.`). 2. It wraps the object with **`reactive()`**, the same deep reactivity used by the Composition API, and stores it as **`this.$data`**. 3. It **proxies each top-level key** onto the instance, so `this.query` reads and writes `this.$data.query`, and templates can use `query` directly. Nested objects and arrays inside the returned object are reactive too, because `reactive()` is deep. ## What is not reactive | Situation | Result | |---|---| | Key returned from `data()` | Reactive, readable as `this.key` | | Property assigned to `this` later, never returned from `data()` | Stored on the instance, **cannot trigger updates** | | Key starting with `_` or `$` | Reactive in `$data`, but **not proxied**: read it as `this.$data._key` | | Object with its own stateful behaviour (a browser API object, a class instance with prototype logic) | Works, but the docs recommend returning plain state only | The practical rule from the Vue docs: include **every** top-level property in the object `data()` returns, using `null` or `undefined` as a placeholder when the value is not available yet. Adding new keys to `this.$data` after creation is possible but not recommended. ## When data() runs and what it can see Vue initialises an options component in a fixed order: props first, then injected values, then `methods`, then `data`, then `computed` and `watch`. So inside `data()`: - **props** can be read (`this.initialQuery`) to seed local state; - **injected** values and **methods** are available; - **computed** properties are **not defined yet**, so reading one returns `undefined`. `data()` receives the instance as its first argument as well, which is why an arrow-function form works: `data: (vm) => ({ query: vm.initialQuery })`. Inside an arrow function `this` is not the instance, so use the argument. ## Things data() must not do - **Be async.** If `data()` returns a Promise, Vue warns that it cannot be async and points to async `setup()` with `<Suspense>` for data needed before rendering. Fetching belongs in a hook or method that assigns into already-declared keys. - **Return shared module-level objects.** `return { filters: sharedFilters }` hands every instance the same object, recreating exactly the sharing the function form exists to prevent. ## Bugs to look for in inherited code When you read an older options component, these are the `data()` mistakes that survive longest: - **Late keys** — a method writes `this.error = e` but `error` is not in `data()`, so the error message never appears in the template. - **Shared defaults** — `data()` returns a constant imported from another module, so instances edit each other's state. - **Copied props that go stale** — `query: this.initialQuery` copies the prop once; later changes to the prop do not reach `query`. That is intended for an initial value, and a bug when the component was meant to follow the prop. - **Private-looking keys** — `_loading` declared in `data()` and read as `this._loading`, which is `undefined`. ## Why interviewers ask This is the first Options API question in most screens: it checks that you know instances are created from a shared definition, that `data()` is a per-instance factory, and that reactivity through `this` is decided by what the function returns. The follow-ups — a property added later that never updates the view, or a `_private` key that reads as `undefined` — test whether you have debugged real options components.

  • In a Vue 3 Options API component, why is this.config !== config right after this.config = config?
    Assigning into reactive state stores the object, but reading `this.config` returns Vue's reactive proxy of it, which is a different object from the original. The original stays a plain, non-reactive object, so mutating it directly does not update the view. Always read and write reactive state through `this`.
  • Can data() read a computed property of the same component?
    No. Vue 3 initialises an options component in the order props, inject, methods, data, then computed and watch, so computed properties do not exist yet while `data()` runs and reading one returns `undefined`. Seed from props or methods instead, or declare the value as computed rather than copying it into data.

saying these in an interview costs you the question

  • data is a function so Vue can re-run it on every re-render.
  • Any property assigned to this later becomes reactive automatically.
  • Returning a module-level object from data() still gives each instance its own copy.
  • data() can be async and Vue waits for it before rendering.
  • A key named _cache in data() can be read as this._cache.