Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

watchEffect should allow multiple onInvalidate calls #3341

Closed
bgoscinski opened this issue Mar 2, 2021 · 2 comments · May be fixed by #12394
Closed

watchEffect should allow multiple onInvalidate calls #3341

bgoscinski opened this issue Mar 2, 2021 · 2 comments · May be fixed by #12394
Labels
has PR A pull request has already been submitted to solve the issue scope: reactivity ✨ feature request New feature or request

Comments

@bgoscinski
Copy link

bgoscinski commented Mar 2, 2021

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.logs 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

@cdauth
Copy link

cdauth commented Dec 26, 2023

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(() => {
		...
	});
});

@bgoscinski
Copy link
Author

closing as this seems to have been implemented in #9927

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
has PR A pull request has already been submitted to solve the issue scope: reactivity ✨ feature request New feature or request
Projects
None yet
4 participants