A component is created, rendered into the DOM, updated some number of times, and eventually removed. Lifecycle hooks let you run code at each of those moments.
The hooks
import { onMounted, onBeforeUnmount, onUpdated } from "vue";
onBeforeMount(() => {}); // after setup, before the first render
onMounted(() => {}); // the DOM exists -- this is the one you will use
onBeforeUpdate(() => {}); // data changed, DOM not yet patched
onUpdated(() => {}); // DOM patched
onBeforeUnmount(() => {}); // still attached -- clean up here
onUnmounted(() => {}); // goneRegister them at the top level of setup. They work by associating
themselves with whichever component is currently being set up, so registering one inside a callback,
a promise or an if either fails or attaches to the wrong component.
The code in <script setup> that is not in a hook is itself the "before created"
moment — it runs first, every time, and is where you declare state.
onMounted
Use it for anything that needs the real DOM, or that should start once the component is on screen.
onMounted(loadPage);That is the whole of the feed's mounted hook after its observer moved into a composable
(lesson 15). Before that it also constructed an IntersectionObserver here — because
new IntersectionObserver({ root: scroller.value }) needs scroller.value to
be a real element, which it is not until mounted.
onMounted does not run on the server. Anything touching
window, document or localStorage belongs here for that reason
alone.
Fetching in onMounted, or not
You can start a request in onMounted, and plenty of code does. You can also just start
it in the setup body — nothing about a fetch needs the DOM:
api.trendingTags().then((t) => (tags.value = t)).catch(() => {});That fires as the component is created, one tick earlier. The demo application does both, choosing
per case: the tag list is fire-and-forget, while the feed's first page is in onMounted
because the observer must exist before slides can be registered.
onBeforeUnmount: the one that matters
Every hook that starts something needs a hook that stops it. Intervals, event
listeners on window, observers, EventSource connections, and any subscription you opened
will otherwise keep running after the component is gone — holding a reference to it, so the whole
component tree stays in memory.
onBeforeUnmount(() => videoEl.value?.pause());One line, and without it every reel you scroll past keeps playing audio.
The pattern for a listener:
function onResize() { width.value = window.innerWidth; }
onMounted(() => window.addEventListener("resize", onResize));
onBeforeUnmount(() => window.removeEventListener("resize", onResize));Note the named function. removeEventListener matches by identity, so an
inline arrow function in both places removes nothing at all — the listener stays, silently, forever.
This is the single most common memory leak in component code.
Lesson 15 shows the better answer: put the pair inside a composable, so the caller cannot forget half of it.
onBeforeUnmount versus onUnmounted
onBeforeUnmount still has the DOM, so it is where you pause a video or read a final
scroll position. onUnmounted is after removal, for anything that does not need the
element. When in doubt use onBeforeUnmount.
Template refs
Sometimes you need the actual element — to call play(), to focus an input, to measure
something. Declare a ref with the same name as the ref attribute:
<video ref="videoEl" ...></video>const videoEl = ref(null);
// null until mounted, so every use is guarded.
function togglePlay() {
const el = videoEl.value;
if (!el) return;
if (el.paused) el.play().catch(() => {});
else el.pause();
}The binding is populated just before onMounted and set back to null on
unmount. Reading it in the setup body always gives null — that is not a bug, the element
does not exist yet.
Refs inside v-for
A static ref="x" inside a loop cannot work — every iteration would claim the same name.
Bind a function instead, and Vue calls it with each element:
:ref="(el) => (slideEls[i] = el)"const slideEls = ref([]);which gives an array indexed the same way as the list, so
slideEls.value[3] is the fourth slide's element.
A ref on a component
ref on a child component gives you the component instance rather than an element. With
<script setup> a component is closed by default — the parent sees
nothing — and the child opts in to exposing specific things:
// in the child
defineExpose({ focus, reset });That default is deliberate and worth respecting. Reaching into a child to call its methods couples the two together in a way props and events do not. Use it for genuinely imperative things — focus, scroll, play — and nothing else.
nextTick
Changing reactive data does not update the DOM immediately. Vue batches changes and applies them on the next tick, which is why this reads the old value:
reels.value.push(...res.items);
console.log(slideEls.value.length); // the OLD length -- not rendered yetawait nextTick() waits for the DOM to catch up:
await nextTick();
observeSlides();The feed appends a page of reels and then has to observe the new slide elements — which cannot
happen until they exist. Without the nextTick, observeSlides() would run over
the previous page's elements and the pager would stop advancing at the end of page one.
That is the whole use of nextTick: I changed data, and now I need the DOM that
resulted from it. Measuring a newly rendered element, focusing an input that just appeared,
scrolling to a row that was just added.
Next: Composables — packaging all of this up so it can be reused.