Skip to the solution
HardFrontend9 min read

What is effectScope?

998 views

The problem

Question presented to candidate: "Vue exports something called effectScope. What is it, what does it collect, and what exactly happens when you call stop() on it?"

What a strong answer should cover:

  • An effect scope is a container: effects created synchronously inside scope.run(fn) (watch, watchEffect, nested scopes) are recorded in it.
  • scope.stop() stops every collected effect, runs every onScopeDispose callback and stops child scopes; scope.active becomes false.
  • Scopes nest: a scope created inside another is stopped with its parent, unless created with effectScope(true) (detached).
  • Every component instance has its own scope; that is how Vue stops a component's watchers on unmount.
  • getCurrentScope() and onScopeDispose() let library code find and hook into the active scope; Vue 3.5 added pause() and resume().
  • run() on a stopped scope warns and returns undefined; the API arrived in Vue 3.2.

Clarifying questions expected:

  • "Are we talking about the API itself, or how composables use it for cleanup?"

Code / implementation expected: Optional β€” create a scope with a watcher and a dispose callback, stop it, and show nothing fires afterwards.

  • effectscope

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 10 sections

Read the code

VueeffectScope playground: run, pause, resume, stop
<script setup>
import { computed, effectScope, onScopeDispose, ref, watch, watchEffect } from "vue";

const count = ref(0);
const log = ref([]);
const say = (msg) => log.value.push(msg);

let scope = null;
const status = ref("no scope");

function start() {
  if (scope) scope.stop(); // never leak a previous scope
  scope = effectScope(true); // detached, so it is not tied to this component
  scope.run(() => {
    const doubled = computed(() => count.value * 2);
    watch(count, (n) => say(`watch: count is ${n}`));
    watchEffect(() => say(`watchEffect: doubled is ${doubled.value}`));
    onScopeDispose(() => say("onScopeDispose: cleanup ran"));
  });
  status.value = "active";
}
function pause() { scope.pause(); status.value = "paused"; }
function resume() { scope.resume(); status.value = "active"; }
function stop() { scope.stop(); status.value = "stopped (active = " + scope.active + ")"; }
</script>

<template>
  <p id="status">scope: {{ status }} | count: {{ count }}</p>
  <button id="start" @click="start">start scope</button>
  <button id="inc" @click="count++">count++</button>
  <button id="pause" @click="pause">pause</button>
  <button id="resume" @click="resume">resume</button>
  <button id="stop" @click="stop">stop</button>
  <ol>
    <li v-for="(line, i) in log" :key="i">{{ line }}</li>
  </ol>
</template>

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 74 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextHard

How do generic <script setup> components work (generic="T")?

Continue