Repository: TanStack/router
Pull request #1648 (2024-05-21 17:54:31)

fix: should render error component when beforeLoad throws an error
beforeLoad wasn't rendering the errorComponent. Added unit tests to cover this case.

nx-cloud[bot]: # ☁️ Nx Cloud Report

CI is running for commit 270c4c2b57cfec4cf37e44ecc83653ccee4ea666.

[📂 Click to track the progress, see the status, the terminal output, and the build insights.](https://cloud.nx.app/cipes/664cdff942011111992bf55f?utm_source=pull-request&utm_medium=comment)


---

Sent with 💌 from [NxCloud.](https://nx.app?utm_source=pull-request&utm_medium=comment)
schiller-manuel: fixes #1647

Patch:
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index a01726f1a..893721af8 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -1717,13 +1717,13 @@ export class Router<
                   handleRedirectAndNotFound(match, err)
                 }
 
-                matches[index] = match = {
+                matches[index] = match = updateMatch(match.id, () => ({
                   ...match,
                   error: err,
                   status: 'error',
                   updatedAt: Date.now(),
                   abortController: new AbortController(),
-                }
+                }))
               }
 
               if (match.paramsError) {


Test patch:
diff --git a/packages/react-router/tests/link.test.tsx b/packages/react-router/tests/link.test.tsx
index 465db79b0..a4a0099e4 100644
--- a/packages/react-router/tests/link.test.tsx
+++ b/packages/react-router/tests/link.test.tsx
@@ -77,7 +77,7 @@ describe('Link', () => {
     ).rejects.toThrow()
   })
 
-  it('when the current route is the root', async () => {
+  test('when the current route is the root', async () => {
     const rootRoute = createRootRoute()
     const indexRoute = createRoute({
       getParentRoute: () => rootRoute,
@@ -132,6 +132,55 @@ describe('Link', () => {
     expect(postsLink).not.toHaveAttribute('data-status', 'active')
   })
 
+  test('when the current route is the root with beforeLoad that throws', async () => {
+    const onError = vi.fn()
+    const rootRoute = createRootRoute({
+      onError,
+      beforeLoad: () => {
+        throw new Error('Something went wrong!')
+      },
+      errorComponent: () => <span>Oops! Something went wrong!</span>,
+    })
+    const indexRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/',
+      component: () => {
+        return (
+          <React.Fragment>
+            <h1>Index</h1>
+            <Link to="/" activeProps={{ className: 'active' }}>
+              Index
+            </Link>
+            <Link to="/posts" inactiveProps={{ className: 'inactive' }}>
+              Posts
+            </Link>
+          </React.Fragment>
+        )
+      },
+    })
+
+    const postsRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/posts',
+      component: () => {
+        return (
+          <React.Fragment>
+            <h1>Posts</h1>
+          </React.Fragment>
+        )
+      },
+    })
+
+    const router = createRouter({
+      routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+    })
+
+    render(<RouterProvider router={router} />)
+
+    expect(await screen.findByText('Oops! Something went wrong!'))
+    expect(onError).toHaveBeenCalledOnce()
+  })
+
   test('when navigating to /posts', async () => {
     const rootRoute = createRootRoute()
     const indexRoute = createRoute({
@@ -342,7 +391,7 @@ describe('Link', () => {
     }
 
     const ErrorComponent = () => {
-      return <h1>Ooops, something went wrong</h1>
+      return <h1>Oops, something went wrong</h1>
     }
 
     const postsRoute = createRoute({
@@ -377,7 +426,7 @@ describe('Link', () => {
     await waitFor(() => expect(onError).toHaveBeenCalledOnce())
 
     expect(
-      await screen.findByText('Ooops, something went wrong'),
+      await screen.findByText('Oops, something went wrong'),
     ).toBeInTheDocument()
   })
 
@@ -619,6 +668,173 @@ describe('Link', () => {
     expect(await screen.findByText('UserId: userId')).toBeInTheDocument()
   })
 
+  test('when navigating to /posts with a beforeLoad that throws an error', async () => {
+    const onError = vi.fn()
+    const rootRoute = createRootRoute()
+    const indexRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/',
+      component: () => {
+        return (
+          <React.Fragment>
+            <h1>Index</h1>
+            <Link to="/posts">Posts</Link>
+          </React.Fragment>
+        )
+      },
+    })
+
+    const PostsComponent = () => {
+      return (
+        <React.Fragment>
+          <h1>Posts</h1>
+        </React.Fragment>
+      )
+    }
+
+    const postsRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: 'posts',
+      beforeLoad: () => {
+        throw new Error('Oops. Something went wrong!')
+      },
+      onError,
+      errorComponent: () => <span>Oops! Something went wrong!</span>,
+      component: PostsComponent,
+    })
+
+    const router = createRouter({
+      context: { userId: 'userId' },
+      routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+    })
+
+    render(<RouterProvider router={router} />)
+
+    const postsLink = await screen.findByRole('link', { name: 'Posts' })
+
+    fireEvent.click(postsLink)
+
+    expect(
+      await screen.findByText('Oops! Something went wrong!'),
+    ).toBeInTheDocument()
+
+    expect(onError).toHaveBeenCalledOnce()
+  })
+
+  test('when navigating to /posts with a beforeLoad that throws an error bubbles to the root', async () => {
+    const rootRoute = createRootRoute({
+      errorComponent: () => <span>Oops! Something went wrong!</span>,
+    })
+
+    const indexRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/',
+      component: () => {
+        return (
+          <React.Fragment>
+            <h1>Index</h1>
+            <Link to="/posts">Posts</Link>
+          </React.Fragment>
+        )
+      },
+    })
+
+    const PostsComponent = () => {
+      return (
+        <React.Fragment>
+          <h1>Posts</h1>
+        </React.Fragment>
+      )
+    }
+
+    const postsRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: 'posts',
+      beforeLoad: () => {
+        throw new Error('Oops. Something went wrong!')
+      },
+      component: PostsComponent,
+    })
+
+    const router = createRouter({
+      context: { userId: 'userId' },
+      routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+    })
+
+    render(<RouterProvider router={router} />)
+
+    const postsLink = await screen.findByRole('link', { name: 'Posts' })
+
+    fireEvent.click(postsLink)
+
+    expect(
+      await screen.findByText('Oops! Something went wrong!'),
+    ).toBeInTheDocument()
+  })
+
+  test('when navigating to /posts with a beforeLoad that throws an error bubbles to the nearest parent', async () => {
+    const rootRoute = createRootRoute({
+      errorComponent: () => <span>Root error</span>,
+    })
+
+    const indexRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/',
+      component: () => {
+        return (
+          <React.Fragment>
+            <h1>Index</h1>
+            <Link to="/posts/$postId" params={{ postId: 'id1' }}>
+              Post
+            </Link>
+          </React.Fragment>
+        )
+      },
+    })
+
+    const PostsComponent = () => {
+      return (
+        <React.Fragment>
+          <h1>Posts</h1>
+          <Outlet />
+        </React.Fragment>
+      )
+    }
+
+    const postsRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: 'posts',
+      errorComponent: () => <span>Oops! Something went wrong!</span>,
+      component: PostsComponent,
+    })
+
+    const postRoute = createRoute({
+      getParentRoute: () => postsRoute,
+      path: '$postId',
+      beforeLoad: () => {
+        throw new Error('Oops. Something went wrong!')
+      },
+    })
+
+    const router = createRouter({
+      context: { userId: 'userId' },
+      routeTree: rootRoute.addChildren([
+        indexRoute,
+        postsRoute.addChildren([postRoute]),
+      ]),
+    })
+
+    render(<RouterProvider router={router} />)
+
+    const postLink = await screen.findByRole('link', { name: 'Post' })
+
+    fireEvent.click(postLink)
+
+    expect(
+      await screen.findByText('Oops! Something went wrong!'),
+    ).toBeInTheDocument()
+  })
+
   test('when navigating to /posts with params', async () => {
     const rootRoute = createRootRoute()
     const indexRoute = createRoute({