Cleanup belongs beside the thing it cleans up, and a composable can register lifecycle hooks because it runs inside the component’s setup scope.
export function usePolling(fetcher, intervalMs = 30000) {
const data = ref(null)
let timer = null
onMounted(() => {
refresh()
timer = setInterval(refresh, intervalMs)
})
onUnmounted(() => clearInterval(timer))
return { data, refresh }
}
A mixin could register hooks too and could not be used twice in one component without the two instances fighting over one variable — the composable has its own closure, so two calls are two intervals. The constraint is that the hooks only register while there is a current instance, so calling a composable after an await inside setup silently registers nothing. The vue/no-lifecycle-after-await lint rule exists for exactly this and is worth enabling.