skip to content

In a Vue 3 `<script setup>` component, where should a data fetch start and where must a DOM measurement happen?

level: juniorimportance: should knowfreq 65%

answer

  1. one of them needs the DOM
  2. template refs start as null
  3. setup runs before the first render
  4. a late response after unmount

basics

~20 s

A DOM measurement must wait for onMounted, because template refs are null until the component has rendered. A fetch needs no DOM, so it can start directly in setup, earlier than onMounted, as long as the component renders a loading state meanwhile.

solid answer

~40 s

`<script setup>` code runs once, before the first render, so no DOM exists and template refs are still `null`; `onBeforeMount` is no better, because it runs right before the first render. Anything that reads elements, such as `offsetWidth` or `getBoundingClientRect()`, belongs in `onMounted`. A fetch does not need the DOM: starting it in setup begins the request a little earlier, and `onMounted` is also acceptable and common, especially for browser-only work. Either way the component renders first with its loading state and fills in when the promise resolves; neither choice delays mounting. What matters more is cleanup: abort or ignore the request in `onUnmounted` so a late response does not write into a component that is gone.

code

vue · 27 lines
vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted, useTemplateRef } from 'vue'

const orders = ref<string[] | null>(null)
const controller = new AbortController()

// fetch: no DOM needed, starts during setup
fetch('/api/orders', { signal: controller.signal })
  .then(r => r.json())
  .then(data => { orders.value = data })
  .catch(e => { if (e.name !== 'AbortError') throw e })

// measurement: needs the rendered element
const box = useTemplateRef<HTMLElement>('box')
onMounted(() => {
  console.log('width', box.value!.offsetWidth)
})

onUnmounted(() => controller.abort())
</script>

<template>
  <div ref="box">
    <p v-if="!orders">Loading...</p>
    <ul v-else><li v-for="o in orders" :key="o">{{ o }}</li></ul>
  </div>
</template>

go deeper

for a junior

Remember that template refs are null until onMounted, so DOM measurements go there, while a fetch can start in setup.

for a middle

Walk through setup, onBeforeMount, render and onMounted, and explain why only the DOM work needs onMounted and why a plain fetch never delays mounting.

for a senior

Cover the late-response race with an AbortController or an unmounted guard, and distinguish an unawaited fetch from top-level await that turns the component async.

for a principal

Set a convention: data loading in composables that own their abort, DOM work only in onMounted, so placement follows from what the code needs.

## The timeline these choices sit on For a Vue 3 component using `<script setup>`, the mount phase runs in this order: 1. **Setup**: the `<script setup>` body runs once. Reactive state, computed values and watchers are created. No DOM exists. 2. **`onBeforeMount`**: state is ready, and the component is about to run its render effect for the first time. Still no DOM. 3. **Render and insert**: Vue renders the template and inserts the resulting DOM; template refs are filled in. 4. **`onMounted`**: the component's DOM exists and is in the parent container, together with that of its synchronous children. ## Measuring the DOM: only onMounted A template ref points at an element only after that element exists. The Vue guide spells it out: you can only access the ref **after the component is mounted**, and it is `null` on the first render. ```ts import { onMounted, useTemplateRef } from 'vue' const chart = useTemplateRef<HTMLDivElement>('chart') onMounted(() => { const { width } = chart.value!.getBoundingClientRect() drawChart(width) }) ``` Reading `chart.value` at the top of `<script setup>` or in `onBeforeMount` returns `null`. `useTemplateRef()` is the Vue 3.5 helper; before 3.5 a `ref(null)` whose variable name matches the template's `ref` attribute does the same job. ## Starting a fetch: setup or onMounted A fetch reads nothing from the DOM, so the choice is about timing and environment, not correctness: | Where | When the request starts | Notes | |---|---|---| | Top of `<script setup>`, without `await` | earliest, before the first render | the component renders its loading state; the response fills in later | | `onMounted` | after the first render and insertion | a common convention for browser-only work | | Top-level `await` in `<script setup>` | before rendering, and rendering waits | makes the component an async dependency that needs a `<Suspense>` ancestor, a separate topic | The first two do not delay mounting: the component shows its loading branch immediately. Starting in setup saves the time between setup and `onMounted`, which is usually small but real for large subtrees. The Options API counterpart is starting the fetch in `created` rather than `mounted`. ## The part people forget: the late response The user may leave before the response arrives. The write then targets a component that no longer exists: its render effect is stopped, so nothing is shown, but the work still runs and anything outside the component the callback touches is still affected. Two standard defences: - **Abort the request**: create an `AbortController`, pass its `signal` to `fetch`, and call `abort()` in `onUnmounted`. - **Ignore the result**: keep an `unmounted` flag set in `onUnmounted` and skip the write if it is true. ## Why onBeforeMount rarely helps `onBeforeMount` sits between setup and the first render. For DOM work it is too early, because nothing is rendered yet. For a fetch it is later than the script body with no benefit, since setup has already finished by then. In the Composition API it is rarely needed for either concern. ## The Options API view of the same choice Options API code faces the same decision with different names: - `created`: data, computed, methods and watchers exist, `$el` does not; a fetch can start here. - `beforeMount`: about to render; still no DOM. - `mounted`: DOM exists; `this.$refs` point at elements; measurements belong here. In both styles the rule is the same: place code by what it needs, the DOM or only state. ## Summary for an interview - **DOM reads and writes** → `onMounted`, because template refs are `null` before it. - **Fetch** → setup (earliest) or `onMounted` (conventional); both render a loading state first. - **`onBeforeMount`** → rarely the answer; it offers neither the DOM nor an earlier start than setup. - **Cleanup** → abort or ignore in `onUnmounted`. Candidates who say "everything in `onMounted`" are not wrong about correctness, but they miss the reason: only the DOM work actually requires it.

  • Is onBeforeMount a good place to start the fetch in Vue 3?
    It works, but it gains nothing. `onBeforeMount` runs after setup, just before the first render, so a request started there begins slightly later than one started directly in setup, and there is still no DOM. It is rarely the right answer for either concern in the Composition API.
  • What changes if you write `const data = await fetch(...)` at the top level of <script setup>?
    The component becomes an async dependency: its setup returns a promise and it will not render until the promise resolves. It must then sit under a `<Suspense>` boundary, otherwise it is not rendered. That is a different design from starting the request without awaiting it and rendering a loading state.

saying these in an interview costs you the question

  • Template refs are available at the top of script setup
  • onBeforeMount already has the component's DOM
  • A fetch started in setup delays the component's mount
  • A fetch must go in onMounted or it will not work
  • A late response after unmount needs no handling