Subscribe on changes!

watch() to include an option to watch permanently

avatar
Jan 4th 2023

What problem does this feature solve?

Our vast Vue 3 application uses composition API. We have a global function that loads data from the database and sets a watch() to automatically persist the data back to the database if any changes were introduced:

// A keyed collection of Vue's reactive proxies.
const cache: Record<string, any /*Vue's reactive proxy*/> = obj()

export function PersistedAppSettings_GetReactiveContainer<T extends Object>(key: string, Init: () => T): T {
  LoadRecordOrCreateNew() {  // Loads the data if it's not in the cache.
    const data = sql.Select(....)
    const proxy = reactive(data)
    watch(data, PersistLazily, { deep: true })
    return proxy
  }

  // If the reactive container has already been created, return it from the cache.
  // Otherwise, load data from SQL and return the Vue3 proxy, which reactive mechanism auto-persists the data after any changes.
  return cache[key] ?? (cache[key] = LoadRecordOrCreateNew())
}

And that works wonderfully! That is, until some of these containers stop getting persisted. After some debugging, it turned out that if the function PersistedAppSettings_GetReactiveContainer() is called in some TSX/JSX components (or it might be called in some components defined via the template syntax, the app is really big) that use composition API, the watch() apparently is silently automatically removed in unmount(), the container stops getting persisted, and the application breaks.

I'd like to request to add an option to the watch() API to request a permanent watcher that is not to be cleaned up, even when called during component setup.

The current API doesn't expose any methods or fields to check if the watch() is still active or not, always invoking watch() will lead to memory leaks in cases when it's not invoked from the component, so I'm struggling to find a working workaround in the meantime. I was thinking about calling unsetCurrentInstance() prior to calling watch() to ensure that the effect will be permanent, but it looks like only getCurrentInstance() is currently exposed in the API (https://github.com/vuejs/core/blob/9c304bfe7942a20264235865b4bb5f6e53fdee0d/packages/runtime-core/src/index.ts#L82)

So, yeah, it's a struggle.

What does the proposed API look like?

// { permanent: true} is what I'm after: watch( object | fn, callback, { permanent: true} ) // The effect of permanent: true is that getCurrentInstance() is ignored, the cleanup function is always returned, and the effect can be cleaned up only by invoking the returned cleanup function.

avatar
Jan 5th 2023

Sounds like you could use a detatched effectScope.

That's also what i.e. pinia uses to keep the stores detatched from the lifecycle of the components that consume them.

A more hacky approach would be to simply create the watcher on the next Tick, when there's no component context anymore.

As the docs explain:

Watchers declared synchronously inside setup() or