Updated June 2026. Tested on Vue 3. Part of the Techalyst Vue 3 series.

setup() is the entry point to the Composition API. It is where you create reactive state, computed values, and functions, and return what the template needs. Most apps today use the <script setup> shorthand, but it compiles down to exactly this function, so understanding setup() explains what <script setup> is really doing.

Where it runs, and why this is not the component

setup() runs after props are resolved but before the component instance is created, earlier than beforeCreate.

props resolved -> setup() -> beforeCreate -> created -> ...

Because the instance does not exist yet, this inside setup() is not the component (it points at the global object). The rule is simple: never use this in setup(). You do not need it, because everything you want is in scope already.

Returning values to the template

Whatever you return from setup() becomes available in the template. Plain variables work for static values, but for anything that should update the view, use ref().

import { ref } from 'vue'

export default {
  setup() {
    const city = ref('London')
    return { city }   // available in the template
  },
}

Inside setup() you read and write a ref through .value; in the template Vue unwraps it for you.

city.value = 'Paris'   // in script
<template>
  <p>{{ city }}</p>     <!-- no .value in template -->
</template>

The two arguments: props and context

setup(props, context) { /* ... */ }

props is a reactive proxy of the component's declared props. Read it directly (no .value), but do not destructure it, that breaks reactivity, use toRefs(props) if you need separate refs.

context is a plain, non-reactive object you can safely destructure into its four parts.

setup(props, { emit, attrs, slots, expose }) { /* ... */ }
  • emit fires a custom event up to the parent. The payload arrives as $event in the parent template.

    emit('relay', city)   // parent: <Child @relay="location = $event" />
    
  • attrs holds non-prop attributes that fall through to the child's root element. It is not reactive, so do not watch it in setup(); read it in a lifecycle hook if you need the latest.

  • slots exposes slot content as functions returning VNodes (slots.city?.()). Also not reactive.

  • expose controls what a parent sees through a template ref. By default a ref on a child gives the parent the whole instance; call expose() to allow-list only what you want public.

    expose({ city, publicMethod })   // parent's $refs sees only these
    

    Note return and expose are independent: return decides what the component's own template can use, expose decides what the parent's ref can reach.

Methods and the closure pattern

Functions defined in setup() and returned become the component's methods. They reach state through closure scope, not this.

setup() {
  const city = ref('London')
  const message = ref('Welcome')   // private: not returned

  function greet() {
    console.log(`${message.value} You are in ${city.value}`)
  }

  return { city, greet }           // message stays private
}

This is the heart of the Composition API: setup() is an outer function, and the functions inside it close over its scope. Anything you do not return stays private to that scope, accessible to the inner functions but invisible from outside, which is a clean, natural way to encapsulate state. (If closures are new to you, see closures and the factory pattern.)

provide and inject in setup

provide() passes data down to any descendant, inject() receives it, one key per call.

import { ref, provide } from 'vue'

setup() {
  const city = ref('London')
  provide('city', city)
  return { city }   // still return it if this template needs it
}
import { inject } from 'vue'

setup() {
  const city = inject('city')   // auto-unwrapped when returned
  return { city }
}

Reactivity is preserved across the boundary: change the ref in the provider and every injecting descendant updates. There is a whole post on provide/inject in this series.

How script setup relates

<script setup> is compile-time sugar over this function. Everything declared at the top level is automatically available to the template (no return), props and emit become the defineProps and defineEmits macros, and expose becomes defineExpose. So the mental model you build here, run-once setup, closures for state, no this, carries straight over.

<script setup>
import { ref } from 'vue'
const city = ref('London')        // no return needed
</script>

Wrapping up

setup() runs once, before the instance exists, so there is no this, you work with closures instead. Return what the template needs, reach for ref to make values reactive, and use the props and context arguments for incoming props and for emit, attrs, slots, and expose. Keep private state unreturned, and remember <script setup> is just a tidier skin over all of this.

More in the series: single file components and script setup and props and emits. Questions welcome below.