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 }) { /* ... */ }
-
emitfires a custom event up to the parent. The payload arrives as$eventin the parent template.emit('relay', city) // parent: <Child @relay="location = $event" /> -
attrsholds non-prop attributes that fall through to the child's root element. It is not reactive, so do not watch it insetup(); read it in a lifecycle hook if you need the latest. -
slotsexposes slot content as functions returning VNodes (slots.city?.()). Also not reactive. -
exposecontrols what a parent sees through a template ref. By default arefon a child gives the parent the whole instance; callexpose()to allow-list only what you want public.expose({ city, publicMethod }) // parent's $refs sees only theseNote
returnandexposeare independent:returndecides what the component's own template can use,exposedecides 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.
All comments ()
No comments yet
Be the first to leave a comment on this post.