8000 Custom equality for watch · Issue #13242 · vuejs/vue · GitHub
[go: up one dir, main page]

Skip to content

Custom equality for watch #13242

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

Open
mitar opened this issue Mar 15, 2025 · 2 comments
Open

Custom equality for watch #13242

mitar opened this issue Mar 15, 2025 · 2 comments

Comments

@mitar
Copy link
Contributor
mitar commented Mar 15, 2025

I know that custom equality for watch has been brought up few times in the past and the general consensus is that it should just be done inside watch callback. But if one wants to use onCleanup, this approach does not work well: onCleanup of previous run is called even if custom equality determines that nothing should be done.

I have the following use case, where I want to reactivity fetch data from the server if and only if fetch URL changes (slightly abridged):

const doc = ref<DocType | null>(null)

watch(
  [() => props.params, () => props.name, () => props.query],
  async ([params, name, query], [oldParams, oldName, oldQuery], onCleanup) => {
    // Do nothing if nothing changed.
    if (isEqual(params, oldParams) && isEqual(name, oldName) && isEqual(query, oldQuery)) {
      return
    }

    const abortController = new AbortController()
    onCleanup(() => abortController.abort())

    const newURL = router.resolve({
      name,
      params,
      query,
    }).href

    try {
      const response = await getURL(newURL, abortController.signal)
      if (abortController.signal.aborted) {
        return
      }

      doc.value = response.doc
    } catch (error) {
      if (abortController.signal.aborted) {
        return
      }
      console.error(error)
      return
    }
  },
  {
    immediate: true,
    deep: true,
  },
)

The issue is that params and query are objects and even if their contents do not change really, but they are just recreated (e.g., inside a template with something like :query="{id: value}"), the watch triggers. Fine, I do deep comparison with isEqual but the issue is that onCleanup aborts the previous request even if isEqual determines that nothing has changed and nothing should be done (so existing in-flight non-aborted-yet fetch should continue to run and finish and update doc ref).

Now, there are few approaches I could take, like doing JSON.stringify on query and params to get a simple string as a reactive value so that existing strict equality would work correctly.

Or I could move abortController to external-to-the-watch variable and be smart to call it only if params change. But then I also have to call it when any outside scope gets destroyed and I have to keep track of that.

I think the easiest and cleanest and more readable would be if I could provide my own equality function. Something like:

  {
    immediate: true,
    deep: true,
    equals: isEqual
  },

Then it would be easy and clear to anyone reading the code, what is the idea here.

@mitar
Copy link
Contributor Author
mitar commented Mar 15, 2025

I could claim that deep: true should already do that, making callback rerun only if any of deeply nested values really changes and is not just recreated, but I think I read in some other issues that this will not happen, and is probably backwards incompatible at this point anyway.

@steffencrespo
< 6ADA span class="Button-content"> Copy link

Thanks for bringing this up — I've run into the same limitation when trying to combine structural equality with abortable side-effects using watch.

The core issue is that the onCleanup() is always invoked regardless of whether the user-defined equality says “nothing changed”, which defeats the point of skipping the fetch logic when params/query are semantically the same.

A few workarounds I've tried:

  • Keeping the AbortController outside the watch and manually aborting only when semantic differences are detected. Works, but feels messy and couples too much logic outside the reactive context.
  • Using JSON.stringify(...) as a reactive dependency (e.g. computed(() => JSON.stringify(props.query))) to get strict equality — readable but potentially fragile, especially with ordering and null vs. undefined.

Totally agree that being able to pass a custom equals function directly into watch() would improve clarity and intent. It would also align more closely with how reactive() and some state management libs handle equality.

If the concern is backward compatibility, perhaps this could be added as an opt-in feature — something like:

watch(source, callback, {
  equals: (a, b) => isEqual(a, b),
})

This would preserve existing behavior but give advanced users more control.

Thanks again for documenting the issue so clearly — following this one closely!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants
0