skip to content

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%

answer

  1. compiled into the same function
  2. bindings reach the template automatically
  3. closed to parent refs by default
  4. needs the SFC compiler
  5. no-build pages and render-function files

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.

solid answer

~40 s

`<script setup>` is compile-time sugar: the SFC compiler turns its body into the component's `setup()`, so it runs once per instance, and every top-level binding, including imports, is usable in the template without a `return`. Three differences matter in practice. A `<script setup>` component is **closed by default**: a parent's template ref sees nothing unless `defineExpose()` lists it, whereas a `setup()` that returns bindings exposes them unless `expose()` narrows them. Top-level `await` is rewritten so the current instance survives it. And the template is compiled into the same scope, so it reads variables directly, without the instance proxy. Write `setup()` explicitly when there is no build step, for render-function or TSX components in `.ts` files, and when adding Composition API code to an existing options component.

code

vue · 14 lines
vue
<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)
function increment() {
  count.value++
}

defineExpose({ increment }) // without this line, a parent's template ref sees nothing
</script>

<template>
  <button @click="increment">{{ count }}</button>
</template>

go deeper

for a junior

Recall that script setup is compiled into setup(), runs per instance, and exposes its top-level bindings to the template.

for a middle

Explain the compiler's additions: automatic bindings, closed-by-default instances with defineExpose, instance-preserving await, and compile-time macros.

for a senior

Choose the right form per file: script setup for SFCs, explicit setup() for no-build pages, render-function libraries and options components gaining composables.

for a principal

Set the codebase convention and its exceptions, so the team knows when explicit setup() is expected rather than a style deviation.

## One function, two ways to write it Every Vue 3 component that uses the Composition API ends up with a `setup()` function. You can write it yourself as an option, or you can write `<script setup>` in a Single-File Component and let the **SFC compiler** generate it. The docs recommend `<script setup>` for SFCs; `setup()` remains the underlying mechanism and the right tool when there is no SFC. The code inside `<script setup>` is compiled as the body of `setup()`, so it runs **every time an instance is created**, not once when the module is imported. That is the first thing to get right in an interview: `<script setup>` is not module-level code. ## What the compiler adds 1. **Automatic template bindings.** Top-level variables, functions and imports are directly usable in the template. With `setup()` you must `return` an object listing them. 2. **Closed instance by default.** The compiler calls `expose()` for you with nothing in it, unless you call `defineExpose()`. A parent's template ref therefore sees no internal state. A `setup()` that returns bindings, by contrast, leaves them visible on the public instance until you call `expose()`. 3. **Instance-preserving `await`.** Top-level `await` compiles to an async setup, and each awaited expression is wrapped so the current component instance is restored afterwards. In a hand-written `async setup()`, code after the first `await` has lost the instance. 4. **Compile-time macros.** `defineProps()`, `defineEmits()`, `defineExpose()` and the others are replaced by the compiler; they do not exist at runtime. 5. **Leaner output.** The template is compiled as a function inlined in the same scope as the setup code, so it reads variables directly instead of going through the instance proxy, which also minifies better. ## Side by side | | `<script setup>` | explicit `setup()` | |---|---|---| | Needs the SFC compiler | yes | no | | Template access to bindings | automatic | via the returned object | | Parent template ref sees | only `defineExpose()` members | returned bindings, unless `expose()` is called | | Props and emits declared with | `defineProps()` / `defineEmits()` | `props` / `emits` options | | Top-level `await` | instance restored after each | instance lost after the first | | Render function instead of template | unusual | natural: return it | ## When explicit setup() is still the right choice - **No build step.** A page that loads Vue from a script tag or an import map has no SFC compiler, so `<script setup>` is unavailable; `setup()` plus a template string or a render function is the way to use the Composition API. - **Render-function and TSX components.** A component in a `.ts` or `.tsx` file returns its render function from `setup()` (or uses the 3.3 function signature of `defineComponent()`). - **Existing options components.** Adding a composable to a component written with options means calling it inside a `setup()` option and returning its bindings; how far to mix the two styles is a separate decision. - **Unsupported combinations.** The docs say that if a need falls outside what `<script setup>` plus a normal `<script>` block supports, switching to an explicit `setup()` is the answer. ## Misconceptions to avoid - `<script setup>` does **not** run once per module; each instance runs it. - It does **not** make every variable public; the opposite is true. - It is **not** a different runtime; the compiled component is an ordinary component with a `setup()`. - Explicit `setup()` is **not** deprecated or discouraged outside SFCs; it is the documented entry point there.

  • Can a <script setup> component also have a normal <script> block?
    Yes. A plain `<script>` block can sit beside it for code that must run once per module, such as named exports, or for options not expressible in `<script setup>`. The docs warn against declaring `props` or `emits` there, and say that if you need more than those scenarios you should switch to an explicit `setup()`.
  • Why is a <script setup> component's compiled output smaller and faster than an equivalent options component?
    Its template is compiled into a function in the same scope as the setup code, so it reads local variables directly rather than through the instance proxy's property lookups. Local names can also be safely shortened by a minifier, which property names on `this` cannot.

saying these in an interview costs you the question

  • Code in <script setup> runs once, when the module is first imported.
  • Every top-level binding in <script setup> is visible to a parent's template ref.
  • script setup is a separate runtime API with no setup() underneath.
  • Explicit setup() is deprecated and should never be written in Vue 3.5.
  • A hand-written async setup() restores the instance after each await like script setup.