Skip to content
226 changes: 126 additions & 100 deletions packages/react-router/tests/errorComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Link,
RouterProvider,
createBrowserHistory,
createLazyRoute,
createRootRoute,
createRoute,
createRouter,
Expand Down Expand Up @@ -38,110 +39,135 @@ afterEach(() => {
cleanup()
})

describe.each([{ preload: false }, { preload: 'intent' }] as const)(
'errorComponent is rendered when the preload=$preload',
(options) => {
describe.each([true, false])('with async=%s', (isAsync) => {
const throwableFn = isAsync ? asyncToThrowFn : throwFn

const callers = [
{ caller: 'beforeLoad', testFn: throwableFn },
{ caller: 'loader', testFn: throwableFn },
]

test.each(callers)(
'an Error is thrown on navigate in the route $caller function',
async ({ caller, testFn }) => {
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: function Home() {
return (
<div>
<Link to="/about">link to about</Link>
</div>
describe.each([true, false])(
'with lazy errorComponent=%s',
(isUsingLazyError) => {
describe.each([{ preload: false }, { preload: 'intent' }] as const)(
'errorComponent is rendered when the preload=$preload',
(options) => {
describe.each([true, false])('with async=%s', (isAsync) => {
const throwableFn = isAsync ? asyncToThrowFn : throwFn

const callers = [
{ caller: 'beforeLoad', testFn: throwableFn },
{ caller: 'loader', testFn: throwableFn },
]

test.each(callers)(
'an Error is thrown on navigate in the route $caller function',
async ({ caller, testFn }) => {
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: function Home() {
return (
<div>
<Link to="/about">link to about</Link>
</div>
)
},
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
beforeLoad: caller === 'beforeLoad' ? testFn : undefined,
loader: caller === 'loader' ? testFn : undefined,
component: function Home() {
return <div>About route content</div>
},
errorComponent: isUsingLazyError ? undefined : MyErrorComponent,
})

if (isUsingLazyError) {
aboutRoute.lazy(() =>
Promise.resolve(
createLazyRoute('/about')({
errorComponent: MyErrorComponent,
}),
),
)
}

const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])

const router = createRouter({
routeTree,
defaultPreload: options.preload,
history,
})

render(<RouterProvider router={router} />)

const linkToAbout = await screen.findByRole('link', {
name: 'link to about',
})

expect(linkToAbout).toBeInTheDocument()
fireEvent.mouseOver(linkToAbout)
fireEvent.focus(linkToAbout)
fireEvent.click(linkToAbout)

const errorComponent = await screen.findByText(
`Error: error thrown`,
undefined,
{ timeout: 1500 },
)
await expect(
screen.findByText('About route content'),
).rejects.toThrow()
expect(errorComponent).toBeInTheDocument()
},
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
beforeLoad: caller === 'beforeLoad' ? testFn : undefined,
loader: caller === 'loader' ? testFn : undefined,
component: function Home() {
return <div>About route content</div>
},
errorComponent: MyErrorComponent,
})

const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])

const router = createRouter({
routeTree,
defaultPreload: options.preload,
history,
})

render(<RouterProvider router={router} />)

const linkToAbout = await screen.findByRole('link', {
name: 'link to about',
})

expect(linkToAbout).toBeInTheDocument()
fireEvent.mouseOver(linkToAbout)
fireEvent.focus(linkToAbout)
fireEvent.click(linkToAbout)

const errorComponent = await screen.findByText(
`Error: error thrown`,
undefined,
{ timeout: 1500 },
)
await expect(
screen.findByText('About route content'),
).rejects.toThrow()
expect(errorComponent).toBeInTheDocument()
},
)

test.each(callers)(
'an Error is thrown on first load in the route $caller function',
async ({ caller, testFn }) => {
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
beforeLoad: caller === 'beforeLoad' ? testFn : undefined,
loader: caller === 'loader' ? testFn : undefined,
component: function Home() {
return <div>Index route content</div>
},
errorComponent: MyErrorComponent,
})

const routeTree = rootRoute.addChildren([indexRoute])

const router = createRouter({
routeTree,
defaultPreload: options.preload,
history,
})

render(<RouterProvider router={router} />)

const errorComponent = await screen.findByText(
`Error: error thrown`,
undefined,
{ timeout: 750 },
test.each(callers)(
'an Error is thrown on first load in the route $caller function',
async ({ caller, testFn }) => {
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
beforeLoad: caller === 'beforeLoad' ? testFn : undefined,
loader: caller === 'loader' ? testFn : undefined,
component: function Home() {
return <div>Index route content</div>
},
errorComponent: isUsingLazyError ? undefined : MyErrorComponent,
})

if (isUsingLazyError) {
indexRoute.lazy(() =>
Promise.resolve(
createLazyRoute('/')({
errorComponent: MyErrorComponent,
}),
),
)
}

const routeTree = rootRoute.addChildren([indexRoute])

const router = createRouter({
routeTree,
defaultPreload: options.preload,
history,
})

render(<RouterProvider router={router} />)

const errorComponent = await screen.findByText(
`Error: error thrown`,
undefined,
{ timeout: 750 },
)
await expect(
screen.findByText('Index route content'),
).rejects.toThrow()
expect(errorComponent).toBeInTheDocument()
},
)
await expect(
screen.findByText('Index route content'),
).rejects.toThrow()
expect(errorComponent).toBeInTheDocument()
},
)
})
})
},
)
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ describe("Store doesn't update *too many* times during navigation", () => {
// that needs to be done during a navigation.
// Any change that increases this number should be investigated.
expect(updates).toBeGreaterThanOrEqual(6) // WARN: this is flaky, and sometimes (rarely) is 7
expect(updates).toBeLessThanOrEqual(7)
expect(updates).toBeLessThanOrEqual(8)
})

test('not found in beforeLoad', async () => {
Expand All @@ -197,7 +197,7 @@ describe("Store doesn't update *too many* times during navigation", () => {
// This number should be as small as possible to minimize the amount of work
// that needs to be done during a navigation.
// Any change that increases this number should be investigated.
expect(updates).toBe(7)
expect(updates).toBe(8)
})

test('hover preload, then navigate, w/ async loaders', async () => {
Expand Down
12 changes: 10 additions & 2 deletions packages/router-core/src/load-matches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,14 +466,22 @@ const executeBeforeLoad = (
if (isPromise(beforeLoadContext)) {
pending()
return beforeLoadContext
.catch((err) => {
.catch(async (err) => {
if (!isRedirect(err)) {
await loadRouteChunk(route)
}
handleSerialError(inner, index, err, 'BEFORE_LOAD')
})
.then(updateContext)
}
} catch (err) {
pending()
handleSerialError(inner, index, err, 'BEFORE_LOAD')
if (!isRedirect(err)) {
return loadRouteChunk(route).then(() => {
handleSerialError(inner, index, err, 'BEFORE_LOAD')
})
}
return handleSerialError(inner, index, err, 'BEFORE_LOAD')
}

updateContext(beforeLoadContext)
Expand Down
Loading
Loading