skip to content

Setup Function

setup(props, context) runs once per instance, receives attrs, slots, emit and expose, and returns bindings or a render function. Interviewers ask why it has no this and what async changes.

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

explore

questions

4

In Vue 3, what arguments does a component's setup() function receive, and what is it allowed to return?

level: juniorimportance: must knowfreq 60%

answer

  1. two arguments, one of them reactive
  2. attrs, slots, emit, expose
  3. no this inside
  4. object of bindings or a function
  5. runs once, before any options hook

basics

~20 s

Vue 3's setup(props, context) receives reactive, read-only props and a context with attrs, slots, emit and expose; it runs once per instance with no this, and returns an object of template bindings or a render function.

solid answer

~40 s

`setup()` is the Composition API entry point for a component written without `<script setup>`. Its first argument, `props`, is a reactive object of the resolved props; read it as `props.title` to stay reactive, and treat it as read-only. The second, the **setup context**, holds `attrs`, `slots` and `emit` (the counterparts of `$attrs`, `$slots` and `$emit`) plus `expose`, which limits what a parent's template ref can reach. Vue calls `setup()` as a plain function, so `this` is `undefined`; everything arrives through the two arguments. It runs once per instance, before any Options API hook. It returns either an object, whose properties the template can use with refs shallow-unwrapped, or a render function, and it should return synchronously unless the component sits under `<Suspense>`.

code

ts · 20 lines
ts
import { defineComponent, ref, computed } from 'vue'

export default defineComponent({
  props: { start: { type: Number, default: 0 } },
  emits: ['change'],
  setup(props, { attrs, slots, emit, expose }) {
    const count = ref(props.start)            // initial snapshot of the prop, on purpose
    const label = computed(() => `Count: ${count.value}`)

    function increment() {
      count.value++
      emit('change', count.value)            // not this.$emit: this is undefined here
    }

    expose({ increment })                    // what a parent's template ref may call

    return { count, label, increment }       // bindings for the template; refs are unwrapped
  },
  template: `<button @click="increment">{{ label }}</button>`,
})

go deeper

for a junior

Recall the signature, setup(props, context), the four context members, that this is undefined, and the two return shapes.

for a middle

Explain props reactivity inside setup, why the context can be destructured but attrs and slots should be read at use time, and ref unwrapping on return.

for a senior

Spot setup mistakes in review: this.$emit, returned VNodes, prop snapshots copied into locals, and async setup without a Suspense ancestor.

for a principal

Decide where explicit setup() still belongs in a codebase that defaults to script setup, such as no-build pages or render-function libraries.

## Where setup() fits In Vue 3, a component can declare a `setup` option: a function Vue calls once when it creates each instance of the component. It is the entry point for the Composition API in two situations: when you use Vue without a build step, and when you integrate Composition API code into a component that otherwise uses options. In Single-File Components, `<script setup>` is the recommended, shorter form, and the SFC compiler turns it into a `setup()` function. ## The first argument: props `props` is a **reactive** object containing the resolved props, after defaults and type casting. Reading `props.title` inside a `computed()` or a watcher tracks it, so those effects update when the parent passes a new value. Two cautions: - **Read through the object.** Copying `props.title` into a local variable takes a one-time snapshot. The body of `setup()` is not itself a reactive effect; Vue pauses dependency tracking while it runs. - **Treat it as read-only.** Props flow down from the parent. Changing a value is done by emitting an event, not by assigning to `props`. ## The second argument: the setup context | Member | What it is | Options API counterpart | |---|---|---| | `attrs` | fallthrough attributes not declared as props or emits | `$attrs` | | `slots` | slot functions passed by the parent | `$slots` | | `emit` | function that emits a declared event | `$emit` | | `expose` | function that limits what a parent's template ref sees | none; the `expose` option is closest | The context object itself is plain and not reactive, so destructuring it in the parameter list, `setup(props, { attrs, slots, emit, expose })`, is safe. `attrs` and `slots` are different: Vue updates them in place whenever the component updates, so always read `attrs.x` or `slots.default` at the point of use instead of destructuring their properties into local variables. ## Why there is no this Vue calls `setup()` as a plain function rather than as a method on the instance, so `this` is `undefined` inside it. That is deliberate. State lives in local variables created with `ref()` and `reactive()`, and the component's capabilities arrive through the two parameters. It also runs before options such as `data`, `computed` and `methods` are applied, so there would be nothing useful to read on `this` anyway. The relationship goes one way: Options API code can read what `setup()` returns through `this`, but `setup()` cannot read options. ## What setup() may return 1. **An object of bindings.** Its properties are available in the template, and in other options through `this`. Refs in it are **shallow-unwrapped**, so the template writes `{{ count }}`, not `{{ count.value }}`. 2. **A render function.** Vue uses it as the component's render function. It closes over the local state directly, which is the natural way to write a component in a `.ts` or `.tsx` file without a template. 3. **Nothing,** for a component whose template needs no bindings. Returning something else produces development warnings: a VNode triggers `setup() should not return VNodes directly - return a render function instead.`, and a non-object value triggers `setup() should return an object.` A VNode is a one-time snapshot; a function lets Vue re-run it inside the component's render effect whenever tracked state changes. ## Timing and synchronous return - `setup()` runs **once per component instance**, when the instance is created, before the first render and before any Options API hook, `beforeCreate` included. - It should return **synchronously**. An `async setup()` returns a promise, which Vue can only wait for when the component has a `<Suspense>` ancestor. - Lifecycle hooks such as `onMounted()` and watchers created in its synchronous body are bound to the instance and cleaned up on unmount. ## Common mistakes - Using `this.$emit` inside `setup()`: `this` is `undefined`; use the `emit` from the context. - Returning `h('div')` instead of `() => h('div')`, which renders once and never updates. - Expecting `setup()` to run again when props change; only effects created inside it re-run.

  • What happens if setup() returns a VNode such as h('div') instead of a render function?
    Vue warns in development that `setup()` should not return VNodes directly and should return a render function instead. A VNode is a single snapshot, so the output would never update. Returning `() => h('div', ...)` lets Vue call it inside the component's render effect, which re-runs whenever the reactive state it reads changes.
  • When does setup() run relative to the component's lifecycle hooks?
    Once, while the instance is being created: before the first render and before every Options API hook, including `beforeCreate`. Hooks registered inside it, such as `onMounted()`, fire later at their own stage. A props change re-runs the effects setup created, never setup itself.

saying these in an interview costs you the question

  • Inside setup(), this points to the component instance just as in methods.
  • setup() runs again whenever the component's props change.
  • The context argument is reactive, so destructuring it breaks updates.
  • Returning h('div') from setup() gives the component a render function.
  • The template must use count.value for refs returned from setup().
open as a page

How do you write a Vue 3 component without an SFC whose setup() returns a render function, and still let a parent call its methods?

level: middleimportance: should knowfreq 35%

basics

~10 s

Define the component with defineComponent, return a render function built with h() from setup(), and call expose({ method }) from the setup context; a parent's template ref then reaches exactly the exposed members.

open as a page

How does Vue 3's <script setup> relate to a setup() function, and when would you still write setup() explicitly?

level: middleimportance: should knowfreq 48%

basics

~20 s
<script setup> compiles into the component's setup(): top-level bindings reach the template directly and the instance is closed to parent refs by default. Write setup() explicitly without an SFC build step or for render-function components.
open as a page

What changes when a Vue 3 component's setup() is async, and why do lifecycle hooks registered after its first await fail?

level: seniorimportance: should knowfreq 36%

basics

~20 s

An async setup() returns a promise, so the component renders only under a Suspense ancestor; and because Vue's current instance is cleared at the first await, hooks, watchers and inject() calls after it are not bound to the component.

open as a page