Skip to content

Conversation

minht11
Copy link

@minht11 minht11 commented Sep 5, 2025

Closes #567

With repeated navigations on the same time, while async module is still being loaded, subsequent navigations wouldn't have any loaders.

Added test which fails without the fix.

Summary by CodeRabbit

  • Bug Fixes
    • Ensures data loaders from lazy-loaded pages are reliably collected and cleared during repeated navigations and when the initial lazy load fails, preventing missing data and race conditions.
  • Performance
    • Improves navigation stability by awaiting in-flight loader collection before applying results, reducing glitches during rapid route changes.
  • Tests
    • Added tests covering repeated-navigation and failed-first-load scenarios to verify loader collection and clearing.

Copy link

coderabbitai bot commented Sep 5, 2025

Walkthrough

Adds exported symbol LOADER_SET_PROMISES_KEY, augments Vue Router RouteMeta to store per-record loader Promise arrays, updates the navigation guard to collect/await/clear those promises across repeated navigations and error paths, and adds tests covering lazy-loaded route navigation behavior.

Changes

Cohort / File(s) Summary
Symbols & Meta
src/data-loaders/symbols.ts, src/data-loaders/meta-extensions.ts
Export new LOADER_SET_PROMISES_KEY symbol and augment vue-router's RouteMeta with optional [LOADER_SET_PROMISES_KEY]?: Promise<void>[].
Navigation guard
src/data-loaders/navigation-guard.ts
Track per-record loader promises via LOADER_SET_PROMISES_KEY, accumulate per-record promises into the global wait list on first and repeated navigations, await Promise.all(...), merge loader sets into to.meta, clear per-record promise storage, and reset on errors.
Tests
src/data-loaders/navigation-guard.spec.ts
Add tests for collecting loaders from lazy-loaded pages across repeated navigation and when initial lazy load fails; import and inspect LOADER_SET_PROMISES_KEY metadata.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective Addressed Explanation
Handle timing-related undefined data when navigating from pages with dynamic params and nested layouts (#567) Changes coordinate loader promise collection across navigations but do not specifically target nested layouts or dynamic param scenarios in code or tests.
Prevent premature clearing of data while component still mounted (#567) Per-record promises are deferred and cleared after resolution, but no explicit lifecycle checks for mounted components in nested layouts were added.
Ensure stability under HMR conditions (#567) No HMR-specific handling or tests were introduced.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Exported symbol LOADER_SET_PROMISES_KEY (src/data-loaders/symbols.ts) The issue describes runtime/timing fixes; adding a public/internal symbol is an API surface change not required by the stated objectives.
Added tests for lazy-load repeated navigation (src/data-loaders/navigation-guard.spec.ts) Tests focus on lazy-load and repeated navigation behavior but do not exercise nested layouts, dynamic params, or HMR scenarios specified in the issue.

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@minht11 minht11 force-pushed the fix/repeated-routes-loaders branch from 4557771 to aa03f3a Compare September 5, 2025 20:03
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/data-loaders/navigation-guard.ts (1)

160-171: Bug: clearing the wrong meta + not cleared on error

  • You set to.meta[LOADER_SET_PROMISES_KEY] = undefined instead of clearing record.meta[...].
  • If module import rejects, promises remain and will be re-awaited on every nav.

Fix by clearing per-record promises and doing it in a finally so it also runs on errors:

-    return Promise.all(lazyLoadingPromises).then(() => {
-      // group all the loaders in a single set
-      for (const record of to.matched) {
-        // merge the whole set of loaders
-        for (const loader of record.meta[LOADER_SET_KEY]!) {
-          to.meta[LOADER_SET_KEY]!.add(loader)
-          to.meta[LOADER_SET_PROMISES_KEY] = undefined
-        }
-      }
-      // we return nothing to remove the value to allow the navigation
-      // same as return true
-    })
+    return Promise.all(lazyLoadingPromises)
+      .then(() => {
+        // group all the loaders in a single set
+        for (const record of to.matched) {
+          for (const loader of record.meta[LOADER_SET_KEY]!) {
+            to.meta[LOADER_SET_KEY]!.add(loader)
+          }
+        }
+        // we return nothing to remove the value to allow the navigation
+        // same as return true
+      })
+      .finally(() => {
+        // clear per-record in-flight promises regardless of success/failure
+        for (const record of to.matched) {
+          record.meta[LOADER_SET_PROMISES_KEY] = undefined
+        }
+      })
🧹 Nitpick comments (1)
src/data-loaders/symbols.ts (1)

7-12: Nit: debug label consistency

Consider pluralizing/aligning the debug string: Symbol('loaderSetPromises') or Symbol('loadersPromises') to better reflect the array of promises and match LOADER_SET_KEY.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e8f8dc4 and aa03f3a.

📒 Files selected for processing (4)
  • src/data-loaders/meta-extensions.ts (2 hunks)
  • src/data-loaders/navigation-guard.spec.ts (1 hunks)
  • src/data-loaders/navigation-guard.ts (3 hunks)
  • src/data-loaders/symbols.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/data-loaders/navigation-guard.spec.ts (3)
playground/src/router.ts (1)
  • router (5-8)
src/data-loaders/symbols.ts (1)
  • LOADER_SET_KEY (5-5)
tests/data-loaders/loaders.ts (2)
  • useDataOne (7-7)
  • useDataTwo (8-8)
src/data-loaders/meta-extensions.ts (1)
src/data-loaders/symbols.ts (1)
  • LOADER_SET_PROMISES_KEY (11-11)
src/data-loaders/navigation-guard.ts (1)
src/data-loaders/symbols.ts (1)
  • LOADER_SET_PROMISES_KEY (11-11)
🔇 Additional comments (6)
src/data-loaders/meta-extensions.ts (2)

11-12: Correct: type-only import

Type-only import of the symbol for use in interface computed keys is right.


74-79: RouteMeta augmentation looks good

Keying by the symbol with Promise<void>[] matches the producer sites.

src/data-loaders/navigation-guard.ts (3)

17-18: Import is correct

Re-export path via meta-extensions is fine.


149-151: Good: persist per-record in-flight promises

Storing promises on record.meta[...] enables awaiting across repeated navigations.


153-157: Good: await prior in-flight loads on repeat nav

Pushing stored promises ensures loaders are collected when async modules are still loading.

src/data-loaders/navigation-guard.spec.ts (1)

184-196: Make async component loading deterministic
Relying on await Promise.resolve() can let the dynamic import finish before the second navigation, making the test flaky. Use a deferred promise to keep the import “in-flight” across both navigations:

--- src/data-loaders/navigation-guard.spec.ts
@@
-    router.addRoute({
-      component: () =>
-        import('../../tests/data-loaders/ComponentWithLoader.vue'),
-    })
+    let resolveImport!: (mod: unknown) => void
+    const pendingImport = new Promise<unknown>(r => (resolveImport = r))
+    router.addRoute({
+      component: () => pendingImport,
+    })
@@
-    void router.push('/fetch')
-    // simulate repeated navigation while the async component is loading
-    await Promise.resolve()
-    await router.push('/fetch')
+    const nav1 = router.push('/fetch')
+    const nav2 = router.push('/fetch')
+    // now resolve the import and let both navigations finish
+    resolveImport(await import('../../tests/data-loaders/ComponentWithLoader.vue'))
+    await Promise.all([nav1, nav2])

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/data-loaders/navigation-guard.ts (1)

154-154: Remove dev log or gate it behind NODE_ENV.

Avoid console noise in production.

-        console.log('REUSE', record.path)
+        // if needed during debugging:
+        // if (process.env.NODE_ENV !== 'production') {
+        //   // eslint-disable-next-line no-console
+        //   console.debug('[data-loaders] reusing pending loader promises for', record.path)
+        // }
src/data-loaders/navigation-guard.spec.ts (1)

28-29: Confirm API surface for LOADER_SET_PROMISES_KEY.

Importing this from the public entry makes it part of the public API. If intended only for tests, consider marking it @internal (and excluding via stripInternal) or importing from an internal path.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between aa03f3a and fa41542.

📒 Files selected for processing (2)
  • src/data-loaders/navigation-guard.spec.ts (2 hunks)
  • src/data-loaders/navigation-guard.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/data-loaders/navigation-guard.spec.ts (2)
src/data-loaders/symbols.ts (2)
  • LOADER_SET_KEY (5-5)
  • LOADER_SET_PROMISES_KEY (11-11)
tests/data-loaders/loaders.ts (2)
  • useDataOne (7-7)
  • useDataTwo (8-8)
src/data-loaders/navigation-guard.ts (1)
src/data-loaders/symbols.ts (1)
  • LOADER_SET_PROMISES_KEY (11-11)
🔇 Additional comments (2)
src/data-loaders/navigation-guard.ts (1)

17-17: Import looks correct.

Symbol is used consistently with meta augmentation.

src/data-loaders/navigation-guard.spec.ts (1)

182-205: Test covers the regression well.

The repeated navigation while the async component is in-flight is exercised and final assertions are precise, including cleanup of LOADER_SET_PROMISES_KEY.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/data-loaders/navigation-guard.ts (1)

160-172: Deduplicate cleanup with finally; ensure symmetric teardown.

Clear LOADER_SET_PROMISES_KEY in a finally block to avoid duplication and guarantee cleanup even if the merge step throws.

-    return Promise.all(lazyLoadingPromises)
-      .then(() => {
-        // group all the loaders in a single set
-        for (const record of to.matched) {
-          // merge the whole set of loaders
-          for (const loader of record.meta[LOADER_SET_KEY]!) {
-            to.meta[LOADER_SET_KEY]!.add(loader)
-          }
-          record.meta[LOADER_SET_PROMISES_KEY] = undefined
-        }
-        // we return nothing to remove the value to allow the navigation
-        // same as return true
-      })
-      .catch((error) => {
-        // If error happens while collecting loaders, reset them
-        // so on next navigation we can try again
-        for (const record of to.matched) {
-          record.meta[LOADER_SET_KEY] = undefined
-          record.meta[LOADER_SET_PROMISES_KEY] = undefined
-        }
-
-        throw error
-      })
+    return Promise.all(lazyLoadingPromises)
+      .then(() => {
+        // group all the loaders in a single set
+        for (const record of to.matched) {
+          for (const loader of record.meta[LOADER_SET_KEY]!) {
+            to.meta[LOADER_SET_KEY]!.add(loader)
+          }
+        }
+        // return nothing to allow the navigation
+      })
+      .catch((error) => {
+        // If error happens while collecting loaders, reset them
+        for (const record of to.matched) {
+          record.meta[LOADER_SET_KEY] = undefined
+        }
+        throw error
+      })
+      .finally(() => {
+        // always clear in-flight bookkeeping
+        for (const record of to.matched) {
+          record.meta[LOADER_SET_PROMISES_KEY] = undefined
+        }
+      })

Also applies to: 173-182

🧹 Nitpick comments (2)
src/data-loaders/navigation-guard.spec.ts (2)

182-205: Make the “repeated navigation while loading” test deterministic.

The dynamic import may resolve before the second push, making the test flaky. Gate the import with a controllable promise so the first navigation is guaranteed in-flight when the second starts.

 it('collects all loaders from lazy loaded pages with repeated navigation', async () => {
   setupApp({ isSSR: false })
   const router = getRouter()
-  router.addRoute({
+  let release!: () => void
+  const gate = new Promise<void>((r) => (release = r))
+  router.addRoute({
     name: '_test',
     path: '/fetch',
-    component: () =>
-      import('../../tests/data-loaders/ComponentWithLoader.vue'),
+    component: async () => {
+      await gate
+      return import('../../tests/data-loaders/ComponentWithLoader.vue')
+    },
   })
 
-  void router.push('/fetch')
-  // simulate repeated navigation while the async component is loading
-  await Promise.resolve()
-  await router.push('/fetch')
+  void router.push('/fetch')
+  // repeated navigation while the async component is still loading
+  const p = router.push('/fetch')
+  release()
+  await p

206-236: Strengthen failure-path assertions.

Optionally assert that the record metadata was reset after the failed first navigation before retrying.

-const firstNavPromise = router.push('/fetch')
+const firstNavPromise = router.push('/fetch')
 await expect(firstNavPromise).rejects.toThrow(Error)
 
+// verify cleanup after failure
+const rec = getRouter().getRoutes().find(r => r.name === '_test')!
+expect(rec.meta[LOADER_SET_PROMISES_KEY]).toBeUndefined()
+// LOADER_SET_KEY is recomputed later; ensure it's cleared too
+expect(rec.meta[LOADER_SET_KEY]).toBeUndefined()
+
 await router.push('/fetch')
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4848247 and 5c14ca8.

📒 Files selected for processing (2)
  • src/data-loaders/navigation-guard.spec.ts (2 hunks)
  • src/data-loaders/navigation-guard.ts (2 hunks)
🔇 Additional comments (4)
src/data-loaders/navigation-guard.spec.ts (1)

28-29: Importing LOADER_SET_PROMISES_KEY for assertions looks fine.

Good to validate the cleanup behavior explicitly.

src/data-loaders/navigation-guard.ts (3)

17-18: New symbol import is correct.

Matches its usage below.


149-151: Persist per-record in-flight promises — OK.

This enables awaiting async component loaders across repeated navigations.


153-157: Reusing in-flight promises on repeated navigations — OK.

Minimal overhead; acceptable for correctness.

…avigations

fix LOADER_SET_PROMISES_KEY being not reset
improve tests

handle error while collecting loaders
@minht11 minht11 force-pushed the fix/repeated-routes-loaders branch from 5c14ca8 to 4e90e60 Compare September 7, 2025 13:32
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/data-loaders/navigation-guard.ts (1)

149-151: Move cleanup to finally block to prevent permanent failures.

If async import rejects, the rejected promise remains in LOADER_SET_PROMISES_KEY and causes Promise.all to keep rejecting on subsequent navigations.

-    return Promise.all(lazyLoadingPromises)
-      .then(() => {
-        // group all the loaders in a single set
-        for (const record of to.matched) {
-          // merge the whole set of loaders
-          for (const loader of record.meta[LOADER_SET_KEY]!) {
-            to.meta[LOADER_SET_KEY]!.add(loader)
-          }
-          record.meta[LOADER_SET_PROMISES_KEY] = undefined
-        }
-        // we return nothing to remove the value to allow the navigation
-        // same as return true
-      })
+    return Promise.all(lazyLoadingPromises)
+      .then(() => {
+        // group all the loaders in a single set
+        for (const record of to.matched) {
+          // merge the whole set of loaders
+          for (const loader of record.meta[LOADER_SET_KEY]!) {
+            to.meta[LOADER_SET_KEY]!.add(loader)
+          }
+        }
+        // we return nothing to remove the value to allow the navigation
+        // same as return true
+      })
+      .finally(() => {
+        // always clear in-flight bookkeeping, even on failures
+        for (const record of to.matched) {
+          record.meta[LOADER_SET_PROMISES_KEY] = undefined
+        }
+      })

Also applies to: 153-156, 168-168

🧹 Nitpick comments (1)
src/data-loaders/navigation-guard.ts (1)

173-182: Redundant cleanup in catch block.

With cleanup moved to finally, the LOADER_SET_PROMISES_KEY reset here becomes redundant.

       .catch((error) => {
         // If error happens while collecting loaders, reset them
         // so on next navigation we can try again
         for (const record of to.matched) {
           record.meta[LOADER_SET_KEY] = undefined
-          record.meta[LOADER_SET_PROMISES_KEY] = undefined
         }
 
         throw error
       })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c14ca8 and 4e90e60.

📒 Files selected for processing (4)
  • src/data-loaders/meta-extensions.ts (2 hunks)
  • src/data-loaders/navigation-guard.spec.ts (2 hunks)
  • src/data-loaders/navigation-guard.ts (2 hunks)
  • src/data-loaders/symbols.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/data-loaders/meta-extensions.ts
  • src/data-loaders/symbols.ts
  • src/data-loaders/navigation-guard.spec.ts

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

Successfully merging this pull request may close these issues.

Data loaders inside nested layouts with params causes undefined data error when existing page in prod and with HRM.
1 participant