Vue / Intermediate: feature state and effect ownership / Lesson 7
Lifecycle, DOM access, and effect cleanup
Match browser work to the component’s lifetime and distinguish state updates from completed rendering.
On this page
Lesson goals
- Register lifecycle hooks in the correct setup context.
- Pair listeners and timers with explicit cleanup.
Give external work an owner
A component can own browser subscriptions, timers, observers, and request coordination in addition to reactive state. These effects must stop when their owner no longer needs them. Lifecycle hooks connect work to mount, update, and unmount boundaries, but their registration must happen synchronously while the active component setup context exists. Calling a hook later inside an arbitrary timeout does not establish the same ownership. Keep the effect’s purpose and cleanup visible together. A component that disappears visually through v-show has not unmounted, so visibility and lifetime are separate decisions.
Delay browser-only access until the appropriate phase
The example starts with a neutral width and reads window only after mount. That keeps browser globals out of setup execution that may also happen during server rendering. The same handler is used for registration and removal, and the listener is not recreated on every reactive update. This is a small local display effect, not a recommendation to duplicate a full responsive-layout system in JavaScript. Prefer CSS for layout where it solves the problem. If an external observer or library is necessary, record its disposal interface and invoke it under the correct owner.
<script setup>
import { ref, onMounted, onUnmounted } from "vue"
const width = ref(null)
function updateWidth() { width.value = window.innerWidth }
onMounted(() => {
updateWidth()
window.addEventListener("resize", updateWidth)
})
onUnmounted(() => window.removeEventListener("resize", updateWidth))
</script>
<template>
<p>{{ width === null ? "Measuring window" : `Width: ${width}` }}</p>
</template>
Requires a Vue 3.5 browser-mounted component. The initial neutral output precedes the width read; unmount removes this handler. Window behavior, rendering, and cleanup were not executed in a browser for this fragment.
Read DOM after the update you require
Changing a ref updates state before Vue necessarily flushes its DOM work. Use nextTick when a handler genuinely needs the rendered result, or a documented post-flush watcher when the effect belongs to a watched state transition. Avoid fixed sleeps that work only on one machine. Template refs can be null before mount or after a conditional removes the node, so check the actual lifetime. Do not use direct DOM writes to compete with Vue over the same rendered property. External libraries should operate through a deliberately isolated integration boundary with cleanup and update rules.
Test a cleanup contract without a real browser
A small disposable subscription helper can be verified with a fake target. The complete JavaScript example records active handlers in a Set and checks that the returned disposer removes the same handler. It does not prove that Vue calls a lifecycle hook, but it validates the helper’s local add/remove contract. Keep these layers separate in the test strategy. A real integration test should mount and unmount the owner and observe relevant behavior. Timer, observer, and subscription APIs differ, so use each API’s supported cancellation mechanism rather than assuming every resource is an event listener.
function subscribe(target, handler) {
target.addEventListener("change", handler)
return () => target.removeEventListener("change", handler)
}
const active = new Set()
const target = {
addEventListener: (_name, handler) => active.add(handler),
removeEventListener: (_name, handler) => active.delete(handler)
}
const dispose = subscribe(target, () => {})
if (active.size !== 1) throw new Error("SUBSCRIPTION_MISSING")
dispose()
if (active.size !== 0) throw new Error("SUBSCRIPTION_LEAK")
Runs under Node using in-memory data only. The expected active count moves from one to zero. No DOM, browser listener, timer, or service is accessed, and the test does not claim framework lifecycle coverage.
Review cached and server-side lifetimes separately
KeepAlive can deactivate a component without destroying its instance, so effects that should pause while inactive may need activation and deactivation handling. Server rendering has a different lifecycle and does not run ordinary browser mount hooks; avoid creating long-lived timers during server setup and expecting unmount cleanup there. Async work can also complete after an owner changes, requiring invalidation as well as disposal. Review repeated mount/unmount cycles, conditionals, route reuse, and cached screens. The goal is a clear owner for each effect and a tested reason for every lifecycle boundary.
Common errors and fixes
A listener accumulates after repeated visits
- Why it happens
- Registration has no matching owner cleanup.
- How to fix it
- Remove the exact handler or dispose the resource on the appropriate lifecycle.
- Prevent it next time
- Test repeated mount and unmount behavior.
window is unavailable during server rendering
- Why it happens
- Browser-only work runs in shared setup execution.
- How to fix it
- Move the browser effect to a client lifecycle boundary.
- Prevent it next time
- Review SSR and client execution separately.
A template ref is used after its node disappears
- Why it happens
- Conditional rendering changed the DOM lifetime.
- How to fix it
- Guard the ref and align the effect with current presence.
- Prevent it next time
- Include conditional removal in integration checks.
Check your understanding
1. A panel is hidden with v-show. Will onUnmounted run at that moment?
Show worked solution
No. The rendered branch remains and the component is not unmounted merely by display hiding. If work should pause while hidden, model that visibility requirement explicitly instead of relying on an unmount hook that will not run.
2. A helper’s disposer test passes. What must a component test still verify?
Show worked solution
Verify that the component registers at the intended time, invokes disposal when the relevant owner ends or deactivates, and does not duplicate work across updates. A pure helper check cannot establish those framework interactions.
Key takeaways
- External effects need explicit ownership and cleanup.
- State and DOM update timing are different.
- Visibility, caching, and server rendering create distinct lifetimes.