What are Effect Scopes in Vue?

You’ve been using effect scopes this whole time, you just don’t realize it.

They’re a fundamental feature in Vue that allow the whole reactivity system to work properly and efficiently.

Let me show you what they are, how they work, and some interesting things that you can do with them.

Effects

In a nutshell, effect scopes allow you to scope effects.

But since that’s not actually a great explanation, let me back up a bit and explain what effects are.

Effects are anything a function does beyond simply returning a value. If a function only returns a value and does nothing else, we call that a pure function:

function add(a, b) {
return a + b
}

All we have are inputs and outputs, with no magic, nothing fancy, and nothing else going on behind the scenes.

We can introduce effects in a number of ways:

  • Modifying variables outside of the function scope
    let counter = 0
    function add(a, b) {
    counter++
    return a + b
    }
  • Fetching data or any other asynchronous operation that uses Promises or async/await
  • User input
  • Timers
  • Rendering to a screen

There are many more, and Vue’s reactivity system (and any other signal library) is entirely based on effects:

const count = ref(0)
const double = computed(() => count.value * 2)
watchEffect(() => console.log(double.value))

This example uses effects twice: first with the computed that will magically update when ever count is updated, and second with the watchEffect that logs out the new value every time the double value is updated.

(and a fourth time because we’re printing information to the screen)

To trigger both of these effects, all we do is write: count.value = 4 and everything else happens in the background. We’re not returning a new value for double, we’re just relying on Vue’s reactivity system to magically do all of that stuff for us.

Effect Scopes

Effect scopes are a way of containing and grouping a set of related effects together so we can “bundle” them. Specifically, we want to be able to stop them easily, which is one of the main reasons why effect scopes were first introduced.

Most of the time, effect scopes are just used by the Vue components themselves when it runs the setup function:

<script setup>
// This whole thing is run inside of an EffectScope
</script>

Then, when that component is unmounted, Vue simply calls scope.stop() and all of the effects are cleaned up and discarded. Without this, the following code would continue logging for the entire lifetime of the application (which would be very bad and not what you want):

const count = ref(0)
watchEffect(() => console.log(count.value))
setInterval(() => count.value += 1, 1000)

If we can’t clean up our effects, this creates a memory and resource leak, but can also create some very nasty bugs that will be difficult to track down and fix.

But effect scopes aren’t just useful for Vue internally. There are also two main use cases that can be valuable for us building applications. The first are temporary effect scopes, or ephemeral effect scopes.

Temporary Effect Scopes

Normally, if we create a watcher, it will live for as long as the component lives. If we want more control over this, we can do a few things to stop or pause the watcher from firing:

// Only let the watcher fire once
watch(
[],
() => {},
{ once: true }
)
// Pause and resume the watcher from firing
const { pause, resume } = watch(...)
pause()
resume()
// Stop the watcher entirely
const stopWatcher = watch(...)
stopWatcher()

But even when we do this, this watcher is still kicking around for the lifetime of the component.

We can use temporary effect scopes to clean up this watcher at any time we want (live demo):

const count = ref(0)
const scope = effectScope()
scope.run(() => {
watch(count, () => {
// Do something interesting
console.log(count.value)
// Immediately stop the scope and kill the watcher
scope.stop()
})
})
// Watcher fires once
count.value = 3
// Watcher no longer exists! So it can't fire anymore
setTimeout(() => count.value = 6, 500)

Here, we replicate the functionality of using once: true. We set count to 3, which triggers the watcher and logs the value of 3 to the console. But then we immediately stop this temporary scope, which cleans up the watcher.

Later, when we update count to 6, the new value is rendered to the screen like we’d expect, but the watcher doesn’t fire. This is because it no longer exists!

In my Advanced Reactivity course, we cover how we can use this to create a waitFor utility that takes a reactive predicate and resolves a Promise when that predicate becomes true:

const isReady = ref(false)
// Only resolves once isReady.value is set to true
await waitFor(() => isReady.value === true)
console.log('isReady is now true')

Long Running Effect Scopes

In addition to creating effect scopes with a shorter lifespan than a component, we can also create effect scopes that live longer, and exist entirely outside of the lifespan of any particular component.

This is very useful for writing composables, and is how the createSharedComposable from VueUse is implemented.

Here’s a basic sketch of what that looks like:

function createSharedComposable(fn) {
let scope, cached
return (...args) => {
if (!scope) {
// Create a detached scope that is cached between calls
scope = effectScope(true)
// Run the composable in the detached scope
cached = scope.run(() => fn(...args))
}
return cached
}
}
// Use createSharedComposable to handle caching
// nd scope management
const useStore = createSharedComposable(() => {
const count = ref(0)
watch(count, (v) => console.log('watch', v))
return { count }
})

By passing true to effectScope, we create a detached effect scope, which is not attached to any parent effect scope.

By default, all effect scopes will be attached to whatever effect scope they are created in. This allows them to be cleaned up when that parent effect scope stops (since an effect scope is itself an effect). This means that our temporary effect scope from earlier will automatically be cleaned up when the component it was created in is unmounted, since we no longer need it anyway!

(Interestingly, components always create their setup effect scopes as detached scopes)

In Advanced Reactivity we dive deeper into the implementation of createSharedComposable, and rebuild it ourselves. It turns out that there are some very interesting things going on in that short function (as is often the case with VueUse code).

Wrapping Up

Although definitely a more advanced topic, understanding what effect scopes are and how they work can make understanding your own Vue code a lot easier.

There are also lots of use cases for them. And though you may not need to reach for this part of Vue’s API very often, it’s always great to have another set of tools you can use.

If you’re interested in learning more about effect scopes and Vue’s reactivity system in general, stay tuned for news on my upcoming course, Advanced Reactivity. By signing up for my newsletter you’ll get all the updates and be notified as soon as it drops!