skip to content

Data, Methods & This

Options components keep state in a data() function, auto-bind methods to the instance and reach everything through the this proxy. Interviewers probe arrow-function pitfalls.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In a Vue 3 Options API component, why do arrow functions break methods and lifecycle hooks, and what does method auto-binding give you?

level: juniorimportance: must knowfreq 55%

answer

  1. lexical this
  2. bind cannot rebind an arrow
  3. each method bound to the instance
  4. data and computed get vm as argument

basics

~20 s

Vue 3 binds every method and hook to the component instance, but an arrow function's this is fixed where it was written, so this is not the instance. Auto-binding means a method passed as a callback keeps its this.

solid answer

~50 s

An arrow function has no `this` of its own: it uses the `this` of the scope where it was written, and `bind()` cannot change that. In an SFC's `<script>`, module scope `this` is `undefined`, so `methods: { save: () => this.post() }` throws when called. Vue 3 binds every entry of `methods`, every lifecycle hook and every `watch` handler to the instance proxy with `bind`, which is exactly what an arrow defeats — hence the docs' advice to avoid arrow functions there. The upside of that binding is that a method keeps its `this` wherever it goes: `setTimeout(this.tick, 1000)` or `addEventListener('resize', this.onResize)` work without an extra wrapper. Arrow functions *inside* a method are fine and useful, because they inherit the method's `this`. `data` and computed getters also receive the instance as their first argument, so an arrow form can use that.

code

js · 28 lines
js
export default {
  data() {
    return { width: 0, ticks: 0, items: [] }
  },
  mounted() {
    // bound methods keep this: no wrapper needed, same reference for removal
    window.addEventListener('resize', this.onResize)
    this.timer = setInterval(this.tick, 1000) // timer id kept off data on purpose: not reactive
  },
  beforeUnmount() {
    window.removeEventListener('resize', this.onResize)
    clearInterval(this.timer)
  },
  methods: {
    onResize() {
      this.width = window.innerWidth
    },
    tick() {
      this.ticks++
    },
    load() {
      // an arrow INSIDE a method inherits the method's this
      return fetch('/api/items').then(res => res.json()).then(items => {
        this.items = items
      })
    }
  }
}

go deeper

for a junior

Recall that methods, hooks and watch handlers in the Options API must be regular functions because arrow functions ignore the this Vue binds.

for a middle

Explain lexical this versus bind, which options Vue binds, and why data and computed can use the instance argument instead of this.

for a senior

Diagnose a legacy component failing on this at call time, and use stable bound methods for listener add and remove pairs.

for a principal

Treat top-level arrow functions in options objects as a lint-enforced rule in a legacy codebase, so the failure is caught in review instead of at the first click.

## Two kinds of this JavaScript functions get `this` in two different ways: - A **regular function** (`function () {}` or the method shorthand `save() {}`) receives `this` **when it is called**, and `fn.bind(obj)` can fix it permanently. - An **arrow function** (`() => {}`) has **no `this` of its own**. It uses the `this` of the surrounding scope at the place it was written — **lexical** `this` — and neither `bind`, `call` nor `apply` can change it. In a Vue single-file component, the `<script>` block is an ES module. At module top level, `this` is `undefined`, and inside an object literal it is still the module's `this`, not the object. ## What Vue 3 binds, and how When an Options API instance is created, Vue binds the component's functions to the **instance proxy** — the object you know as `this`: | Option | How Vue passes the instance | Arrow function works? | |---|---|---| | `methods` | Each function is `bind`-ed to the instance once | No — `this` stays lexical | | Lifecycle hooks (`created`, `mounted`, …) | Each hook is bound to the instance | No | | `watch` handlers | Called with the instance as `this` | No | | `data` | Called with `this` **and** the instance as first argument | Yes, via the argument | | `computed` getters | Bound, **and** given the instance as first argument | Yes, via the argument | The Vue docs state it for each: avoid arrow functions when declaring methods, watch callbacks and lifecycle hooks, because they will not have access to the instance via `this`. For `data` and `computed`, the docs show the argument form: `data: (vm) => ({ a: vm.myProp })`. ## What the failure looks like ```js export default { data: () => ({ saving: false }), // fine: uses no this methods: { save: () => { this.saving = true } // TypeError: this is undefined }, mounted: () => { this.save() } // same TypeError } ``` The error appears only when the function is called, not when the component compiles, so the component may render and fail on the first click. ## Why auto-binding is useful Because each method is bound once at creation, **a method carries its instance wherever it is passed**: 1. `setTimeout(this.tick, 1000)` — `tick` still sees the component. 2. `window.addEventListener('resize', this.onResize)` — and the same reference can be passed to `removeEventListener` later, because it is one stable bound function per instance. 3. Passing `this.validate` as a callback into a helper or a child component's prop. Without binding, each of these would need a wrapper such as `() => this.tick()`. ## Where arrow functions are right - **Inside** a method or hook: `fetch(url).then(res => { this.items = res })` — the arrow inherits the method's `this`, which is the instance. - Inline template handlers: `@click="() => select(item)"` — template expressions are compiled against the instance, so no `this` is involved. - The `data` and computed argument form, where the instance arrives as a parameter. ## Handlers named by string The `watch` option also accepts a **method name as a string**: `watch: { query: 'search' }` runs `this.search` when `query` changes. Vue looks the name up among the instance's bound methods, so the handler gets the correct `this` without any function literal in the `watch` block. If the name does not match a method, development builds warn `Invalid watch handler specified by key "search"`. In legacy code this form is a useful way to sidestep the arrow problem entirely: the logic lives in a normal method, and the watcher just points at it. ## Reading an older component When you inherit an options component that fails with `Cannot read properties of undefined` on `this`, look for arrow functions at the top level of `methods`, hooks and `watch`. Convert them to the method shorthand; leave arrows nested inside method bodies as they are. A related dev warning — `Method "save" has type "undefined" in the component definition. Did you reference the function correctly?` — means a `methods` entry is not a function at all, typically a misspelled import.

  • Why can this.onResize be passed to both addEventListener and removeEventListener in a Vue 3 options component?
    Vue binds each method once, when the instance is created, and stores that bound function on the instance. Every read of `this.onResize` returns the same function object, so `removeEventListener` receives the exact reference that was added. Creating a fresh wrapper such as `() => this.onResize()` in each call would break removal.
  • A Vue 3 computed option is written as fullName: () => this.first + ' ' + this.last. How do you fix it without changing it to a regular function?
    Use the argument Vue passes: computed getters receive the instance as their first parameter, so `fullName: (vm) => vm.first + ' ' + vm.last` works. The same applies to `data`, as in `data: (vm) => ({ query: vm.initialQuery })`. Methods, hooks and watch handlers get no such argument, so they must use regular functions.

saying these in an interview costs you the question

  • Arrow functions are fine for methods because Vue rebinds this anyway.
  • An arrow method fails at compile time, so it can never reach production.
  • Arrow functions must also be avoided inside method bodies.
  • Passing this.tick to setTimeout loses this, so a wrapper is always required.
  • Vue binds methods lazily, the first time a template calls them.
open as a page

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%

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.

open as a page

In a Vue 3 Options API component, how does this resolve a name such as this.total across data, props, computed and methods?

level: middleimportance: should knowfreq 40%

basics

~20 s

Vue 3's this is a proxy that looks a name up in a fixed order: setup() bindings, data, props, then methods, computed and inject, then built-in $ properties, then app globalProperties. Writes follow the same order, and props are read-only.

open as a page

An older Vue 3 options component calls this.$el.querySelector('input') in mounted and broke after its template gained a second root element; why, and what should replace it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

In Vue 3, a component with several root nodes has no single root element, so this.$el is a placeholder text node that has no querySelector. Put ref="input" on the element and read this.$refs.input after mount.

open as a page