diff --git a/.changeset/fix-aborted-loader-undefined-loaderdata.md b/.changeset/fix-aborted-loader-undefined-loaderdata.md
new file mode 100644
index 0000000000..8f0e84d777
--- /dev/null
+++ b/.changeset/fix-aborted-loader-undefined-loaderdata.md
@@ -0,0 +1,5 @@
+---
+'@tanstack/router-core': patch
+---
+
+Resolve a match to `error` instead of `success` when its loader is aborted before producing data. Previously an aborted loader could leave the match in `success` with `loaderData` undefined, so the route component rendered while `useLoaderData()` returned `undefined` despite its non-undefined type.
diff --git a/packages/react-router/tests/loaders.test.tsx b/packages/react-router/tests/loaders.test.tsx
index d9b5968d72..b727a16064 100644
--- a/packages/react-router/tests/loaders.test.tsx
+++ b/packages/react-router/tests/loaders.test.tsx
@@ -751,12 +751,39 @@ test('throw abortError from loader upon initial load with basepath', async () =>
render()
- const indexElement = await screen.findByText('Index route content')
- expect(indexElement).toBeInTheDocument()
- expect(screen.queryByTestId('index-error')).not.toBeInTheDocument()
+ const errorElement = await screen.findByTestId('index-error')
+ expect(errorElement).toBeInTheDocument()
+ expect(screen.queryByText('Index route content')).not.toBeInTheDocument()
expect(window.location.pathname.startsWith('/app')).toBe(true)
})
+test('aborted loader does not render the route component with undefined loaderData', async () => {
+ const rootRoute = createRootRoute({})
+
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ loader: async (): Promise<{ value: string }> => {
+ return Promise.reject(new DOMException('Aborted', 'AbortError'))
+ },
+ component: () => {
+ const data = indexRoute.useLoaderData()
+ return
value: {data.value}
+ },
+ errorComponent: () => (
+ indexErrorComponent
+ ),
+ })
+
+ const routeTree = rootRoute.addChildren([indexRoute])
+ const router = createRouter({ routeTree, history })
+
+ render()
+
+ expect(await screen.findByTestId('index-error')).toBeInTheDocument()
+ expect(screen.queryByTestId('index-content')).not.toBeInTheDocument()
+})
+
test('cancelMatches after pending timeout', async () => {
function getPendingComponent(onMount: () => void) {
const PendingComponent = () => {
diff --git a/packages/router-core/src/load-matches.ts b/packages/router-core/src/load-matches.ts
index f901a0c97d..cbf37da0d1 100644
--- a/packages/router-core/src/load-matches.ts
+++ b/packages/router-core/src/load-matches.ts
@@ -731,13 +731,19 @@ const runLoader = async (
match._nonReactive.loaderPromise = undefined
return
}
- inner.updateMatch(matchId, (prev) => ({
- ...prev,
- status: prev.status === 'pending' ? 'success' : prev.status,
- isFetching: false,
- context: buildMatchContext(inner, index),
- }))
- return
+ // a previous load already produced data, so keep showing it instead of
+ // surfacing the abort
+ if (inner.router.getMatch(matchId)?.status !== 'pending') {
+ inner.updateMatch(matchId, (prev) => ({
+ ...prev,
+ isFetching: false,
+ context: buildMatchContext(inner, index),
+ }))
+ return
+ }
+ // no loaderData yet: fall through to error handling rather than
+ // resolving to `success`, which would render the route component with
+ // loaderData undefined
}
const pendingPromise = match._nonReactive.minPendingPromise
diff --git a/packages/solid-router/tests/loaders.test.tsx b/packages/solid-router/tests/loaders.test.tsx
index 063660f592..4ecee0521e 100644
--- a/packages/solid-router/tests/loaders.test.tsx
+++ b/packages/solid-router/tests/loaders.test.tsx
@@ -342,12 +342,39 @@ test('throw abortError from loader upon initial load with basepath', async () =>
render(() => )
- const indexElement = await screen.findByText('Index route content')
- expect(indexElement).toBeInTheDocument()
- expect(screen.queryByTestId('index-error')).not.toBeInTheDocument()
+ const errorElement = await screen.findByTestId('index-error')
+ expect(errorElement).toBeInTheDocument()
+ expect(screen.queryByText('Index route content')).not.toBeInTheDocument()
expect(window.location.pathname.startsWith('/app')).toBe(true)
})
+test('aborted loader does not render the route component with undefined loaderData', async () => {
+ const rootRoute = createRootRoute({})
+
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ loader: async (): Promise<{ value: string }> => {
+ return Promise.reject(new DOMException('Aborted', 'AbortError'))
+ },
+ component: () => {
+ const data = indexRoute.useLoaderData()
+ return value: {data().value}
+ },
+ errorComponent: () => (
+ indexErrorComponent
+ ),
+ })
+
+ const routeTree = rootRoute.addChildren([indexRoute])
+ const router = createRouter({ routeTree })
+
+ render(() => )
+
+ expect(await screen.findByTestId('index-error')).toBeInTheDocument()
+ expect(screen.queryByTestId('index-content')).not.toBeInTheDocument()
+})
+
test('reproducer #4245', async () => {
const LOADER_WAIT_TIME = 500
const rootRoute = createRootRoute({})