From 178d4a93c59672eed2bcd0745fe0fed80c0abfef Mon Sep 17 00:00:00 2001 From: leesb971204 Date: Thu, 16 Jan 2025 17:12:18 +0900 Subject: [PATCH 1/3] fix(react-router): ensure fetch abort works correctly even with pendingComponent Signed-off-by: leesb971204 --- packages/react-router/src/router.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts index 12aec91f90..7684d443e1 100644 --- a/packages/react-router/src/router.ts +++ b/packages/react-router/src/router.ts @@ -2014,6 +2014,9 @@ export class Router< // eslint-disable-next-line @typescript-eslint/require-await onReady: async () => { // eslint-disable-next-line @typescript-eslint/require-await + const hasPendingFetches = this.state.pendingMatches?.some( + (match) => !!match.isFetching, + ) this.startViewTransition(async () => { // this.viewTransitionPromise = createControlledPromise() @@ -2044,7 +2047,9 @@ export class Router< isLoading: false, loadedAt: Date.now(), matches: newMatches, - pendingMatches: undefined, + pendingMatches: hasPendingFetches + ? s.pendingMatches + : undefined, cachedMatches: [ ...s.cachedMatches, ...exitingMatches.filter((d) => d.status !== 'error'), @@ -2214,8 +2219,14 @@ export class Router< let rendered = false const triggerOnReady = async () => { + const hasPendingFetches = this.state.pendingMatches?.some( + (match) => !!match.isFetching, + ) + if (!rendered) { - rendered = true + if (!hasPendingFetches) { + rendered = true + } await onReady?.() } } From 7fc6d112fa3212cb4a1ecefbbd474f7a3ded1905 Mon Sep 17 00:00:00 2001 From: leesb971204 Date: Mon, 20 Jan 2025 10:46:36 +0900 Subject: [PATCH 2/3] test(react-router): add test case for ensure fetch abort works correctly even with pendingComponent Signed-off-by: leesb971204 --- packages/react-router/tests/router.test.tsx | 83 +++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/packages/react-router/tests/router.test.tsx b/packages/react-router/tests/router.test.tsx index 8f6ffcb67a..3dad08262c 100644 --- a/packages/react-router/tests/router.test.tsx +++ b/packages/react-router/tests/router.test.tsx @@ -1250,3 +1250,86 @@ describe('history: History gives correct notifcations and state', () => { unsub() }) }) + +describe('router loader abort behavior', () => { + it('should abort previous route loader even with pendingComponent present', async () => { + const abortSpy = vi.fn() + let resolveLoader: (value: unknown) => void + + const rootRoute = createRootRoute({ + component: () => { + const navigate = useNavigate() + return ( +
+ + +
+ ) + }, + }) + + const routeA = createRoute({ + getParentRoute: () => rootRoute, + path: '/a', + component: () =>

Route A

, + pendingComponent: () =>

Loading Route A...

, + gcTime: 0, + staleTime: 0, + pendingMs: 0, + pendingMinMs: 1000, + }) + + const routeB = createRoute({ + getParentRoute: () => rootRoute, + path: '/b', + component: () =>

Route B

, + pendingComponent: () =>

Loading Route B...

, + gcTime: 0, + staleTime: 0, + pendingMs: 0, + pendingMinMs: 1000, + loader: async ({ abortController }) => { + abortController.signal.addEventListener('abort', () => { + abortSpy() + console.log('Loader aborted!') + }) + + await new Promise((resolve) => { + resolveLoader = resolve + }) + + return null + }, + }) + + const router = createRouter({ + routeTree: rootRoute.addChildren([routeA, routeB]), + history: createMemoryHistory({ + initialEntries: ['/a'], + }), + }) + + render() + await act(() => router.load()) + + expect(screen.getByText('Route A')).toBeInTheDocument() + + await act(async () => { + fireEvent.click(screen.getByText('Go to B')) + }) + + await screen.findByText('Loading Route B...') + + await act(async () => { + fireEvent.click(screen.getByText('Go to A')) + await new Promise((resolve) => setTimeout(resolve, 0)) + }) + + expect(abortSpy).toHaveBeenCalled() + + resolveLoader!(null) + }) +}) From eeeaeda84c5ca9535aec516a5dd096d83a8fb691 Mon Sep 17 00:00:00 2001 From: leesb971204 Date: Thu, 23 Jan 2025 16:31:31 +0900 Subject: [PATCH 3/3] fix(react-router): fix logic to conditionally set pendingMatches based on browser location changes Signed-off-by: leesb971204 --- packages/react-router/src/router.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts index 7684d443e1..116edd7b2d 100644 --- a/packages/react-router/src/router.ts +++ b/packages/react-router/src/router.ts @@ -1959,8 +1959,11 @@ export class Router< try { const next = this.latestLocation const prevLocation = this.state.resolvedLocation + const actualBrowserLocation = this.state.location const hrefChanged = prevLocation.href !== next.href const pathChanged = prevLocation.pathname !== next.pathname + const browserLocationChanged = + actualBrowserLocation.href !== next.href // Cancel any pending matches this.cancelMatches() @@ -2047,9 +2050,10 @@ export class Router< isLoading: false, loadedAt: Date.now(), matches: newMatches, - pendingMatches: hasPendingFetches - ? s.pendingMatches - : undefined, + pendingMatches: + hasPendingFetches && browserLocationChanged + ? s.pendingMatches + : undefined, cachedMatches: [ ...s.cachedMatches, ...exitingMatches.filter((d) => d.status !== 'error'),