watchEffect should allow multiple onInvalidate calls
What problem does this feature solve?
Right now each effect 'remembers' only the last callback passed to onInvalidate
:
watchEffect((onInvalidate) => {
// do some work and then...
onInvalidate(() => console.log('foo'))
onInvalidate(() => console.log('bar'))
})
the first console.log
won't ever be called. While in this example it's trivial to shove both console.log
s to a single cleanup function it can get pretty hairy when the effect gets complex.
My exact use case was reacting to audio device selection changes by creating multiple MediaStream instances conditionally and hooking them up with some other parts of the app. Since MediaStreams are created conditionally I had several if statements and each of them called onInvalidate
passing some cleanup function. I was surprised that some resources weren't properly released when I changed the devices.
This feature request doesn't require any end user facing API changes, rather changing of Vue internals to keep track of multiple callbacks instead of just the last one.
In the meantime I solved my use case by creating a crude wrapper around watchEffect
that does what I need. While it was easy to implement this workaround I think it would greatly improve watchEffect
ergonomics if we had it in Vue out of the box.
import { watchEffect as vueWatchEffect } from 'vue'
/**
* Vue's watchEffect remembers only the last cleanup function. This wrapper
* allows to register multiple cleanup functions
*/
export const watchEffect: typeof vueWatchEffect = (effect, opts) => {
return vueWatchEffect((onInvalidate) => {
let invalidated = false
const cleanupFns = new Set<() => void>()
const registerCleanupFn = (cleanupFn: () => void) => {
if (invalidated) {
cleanupFn()
} else {
cleanupFns.add(cleanupFn)
}
}
const doCleanup = () => {
invalidated = true
cleanupFns.forEach((cleanupFn) => {
cleanupFn()
})
cleanupFns.clear()
}
onInvalidate(doCleanup)
effect(registerCleanupFn)
}, opts)
}
What does the proposed API look like?
no changes
My personal workaround is this function:
function fixOnCleanup(onCleanup: (cleanupFn: () => void) => void): (cleanupFn: () => void) => void {
const cleanupFns: Array<() => void> = [];
onCleanup(() => {
for (const cleanupFn of cleanupFns) {
cleanupFn();
}
});
return (cleanupFn: () => void) => {
cleanupFns.push(cleanupFn);
};
}
watch(valueRef, (newValue, oldValue, onCleanup_) => {
const onCleanup = fixOnCleanup(onCleanup_);
onCleanup(() => {
...
});
});