core icon indicating copy to clipboard operation
core copied to clipboard

Suspended component still trigger watchers while being switched out

Open posva opened this issue 2 years ago • 4 comments

Vue version

latest

Link to minimal reproduction

https://sfc.vuejs.org/#eNq1VMGOmzAQ/ZWRL2QlAneUbLt/UHWvXFgYwKmxLduQVhH/3sF2GtioaaXVKorAM2+ex2+eubAXrbNpRFawg60N1w4sulE/lxKAD1oZBxewfSWEOn/HNgVt1MQbhBlaowZIqDhZgb9VBqV7/SXrmM/yW2jZ6R78Yu/RPhbh9HO1ktaBUaNDOK762d3InxZc7G6XeGiShhJKASz/dpS140qCU10ncPcElyUMAZZNlRgX/s3qeFwf6sum6WKVWojmUh7yoCMpSAuHgxaVQ6/n4W10jjb/Wgte/ziWLHRRMp+F2JRH5gHqSWjZmwg5vI5Wo7SB0EdqRWJKagIKbonUN18yyK8l+armkP/piKUsjGE/VDo7WSXJBF6OMiZsyYqrQCWjYSzrkvXOaVvkuW39hE42U6bL6S0zo3R8wAztsH8z6mzREHHJ0hVHTsEJzZ5ka9CgecT5DnrHGyWf6Shbkz2087lydZ8Clyes3dbGwWhKYCZUt0sCKfjBC1U1XHaJ99LWjoHp6jkP8HvsyF/H57WbUgihKOpms7qvZIehncVcgWhOr2A+DNhwmlwBzhBViLZitH0BiVbW+as1U9kjDzZ8isZYnS54LqS2Fnl3HW/C/q+k4eLhTw9ssK1GQQXRvZW/RX5Et7sYhflMff2+HxA4dGmobyPhQhZclv7xjy/Anfpe2b/JP/8GNyvngQ==

Steps to reproduce

  • click the button

What is expected?

only one watcher should trigger

What is actually happening?

both, the watcher of the leaving and entering component are benig triggered

System Info

No response

Any additional comments?

In this scenario the update of the variable being watched is what triggers the switch

  • https://github.com/vuejs/router/issues/1565

Is this just a race condition? Since the old component still needs to display, I would say it's normal for it to render due to watchers. This would require vue router to have a special handling of the current route so

posva avatar Oct 03 '22 06:10 posva

let me make a supplement,in Vue document

When a revert happens, fallback content will not be immediately displayed. Instead, <Suspense> will display the previous #default content while waiting for the new content and its async dependencies to be resolved.

"<Suspense> will display the previous #default content" ,It means that unmounted old component will delay. so when you switch ParentSync to ParentAsync ,it will trigger watch in ParentSync as it's still not unmounted. So, I'm not sure if this is a bug.

To avoid this situation,use timeout=0 prop,as follow:

<Suspense timeout=0>
  <component :is="route" />
</Suspense>

moushicheng avatar Oct 05 '22 11:10 moushicheng

@moushicheng @posva This wont do, because original issue arose from a vue-router use case. When I'm navigating between pages, I want the old page to be displayed, while new page is being loaded in the background thanks to Suspense. Unfortunately this is currently impossible in vue-router, because the old page keeps responding to effects/watchers while being switched out.

I'm quite baffled by this, because I don't know how to fetch data in pages based on route parameters, so that old pages don't trigger re-fetch while navigating to different page. Nuxt has the same problem.

adamjedlicka avatar Oct 06 '22 08:10 adamjedlicka

You can check the data fetching documentation in vue router, there are a few examples. There is also an ongoing RFC for a more advanced data fetching api in the rfcs repo

posva avatar Oct 06 '22 10:10 posva

@posva Thanks, I looked into both, but unfortunately neither is sufficient for us.

First, about the data fetching in the Vue Router documentation. Data fetching in beforeRouteEnter or in the created method are kinda meh since Vue 3 with setup came. I need application context for the data fetching (is the user signed? what config is in the store? what cookies are set? what route parameters are there?...) which are only accessible via composables in the setup method. (BTW, wishful thinking, it would be great if we could use composables relying on app context (provide/inject) in route guards).

Fetching in created is now even less desirable since introduction of Suspense.

adamjedlicka avatar Oct 06 '22 13:10 adamjedlicka

Here is a unit test:

  test('post flush watchers in toggled components', async () => {
    let cnt = 0

    const CompA = {
      template: `<div>A</div>`,
      setup: async () => {
        const route = inject<any>('route')

        watch(
          () => route.value,
          () => cnt++,
          { immediate: true, flush: 'post' }
        )
      }
    }

    const CompB = {
      template: `<div>B</div>`,
      setup: async () => {
        const route = inject<any>('route')

        watch(
          () => route.value,
          () => cnt++,
          { immediate: true, flush: 'post' }
        )
      }
    }

    const route = shallowRef(CompA)

    const Parent = {
      template: `
        <Suspense>
          <Component :is="route" />
        </Suspense>
      `,
      setup: () => {
        provide('route', route)
        return { route }
      }
    }

    const root = nodeOps.createElement('div')
    render(h(Parent), root)

    // wait for flush
    await nextTick()
    // wait for child async setup resolve
    await nextTick()

    expect(serializeInner(root)).toBe(`<div>A</div>`)
    expect(cnt).toBe(1)

    route.value = CompB

    // wait for flush
    await nextTick()
    // wait for child async setup resolve
    await nextTick()

    expect(serializeInner(root)).toBe(`<div>B</div>`)
    expect(cnt).toBe(2)
  })

adamjedlicka avatar Nov 02 '22 08:11 adamjedlicka

@posva Hey. So I created a PR with a fix here: https://github.com/vuejs/core/pull/7009 . Could you please look at it and ideally merge it?

adamjedlicka avatar Nov 02 '22 10:11 adamjedlicka

I see some issues linked here but hey seem to look like they are solved but I this this issue still occurs. Or it might be because I have this problem using nuxt 3.

Currently using flush: 'post' solved the issue

metkm avatar Mar 21 '24 07:03 metkm