Skip to content

Commit df80b28

Browse files
committed
feat(loaders): allow errors as a function
1 parent 646e5bf commit df80b28

File tree

4 files changed

+98
-11
lines changed

4 files changed

+98
-11
lines changed

src/data-loaders/createDataLoader.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ export interface DefineDataLoaderOptionsBase<isLazy extends boolean> {
120120

121121
/**
122122
* List of _expected_ errors that shouldn't abort the navigation (for non-lazy loaders). Provide a list of
123-
* constructors that can be checked with `instanceof`.
123+
* constructors that can be checked with `instanceof` or a custom function that returns `true` for expected errors.
124124
*/
125-
errors?: Array<new (...args: any) => any>
125+
errors?: Array<new (...args: any) => any> | ((reason?: unknown) => boolean)
126126
}
127127

128128
export const toLazyValue = (

src/data-loaders/defineLoader.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ export function defineBasicLoader<Data, isLazy extends boolean>(
9494
): Promise<void> {
9595
const entries = router[LOADER_ENTRIES_KEY]!
9696
const isSSR = router[IS_SSR_KEY]
97+
98+
// ensure the entry exists
9799
if (!entries.has(loader)) {
98100
entries.set(loader, {
99101
// force the type to match

src/data-loaders/navigation-guard.spec.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,4 +471,78 @@ describe('navigation-guard', () => {
471471
expect(router.currentRoute.value.fullPath).toBe('/#ok')
472472
})
473473
})
474+
475+
describe('errors', () => {
476+
class CustomError extends Error {}
477+
478+
it('lets the navigation continue if the error is expected', async () => {
479+
setupApp({ isSSR: false })
480+
const router = getRouter()
481+
const l1 = mockedLoader({ errors: [CustomError] })
482+
router.addRoute({
483+
name: '_test',
484+
path: '/fetch',
485+
component,
486+
meta: {
487+
loaders: [l1.loader],
488+
},
489+
})
490+
491+
router.push('/fetch')
492+
await vi.runOnlyPendingTimersAsync()
493+
l1.reject(new CustomError('expected'))
494+
await router.getPendingNavigation()
495+
expect(router.currentRoute.value.fullPath).toBe('/fetch')
496+
})
497+
498+
it('fails the navigation if the error is not expected', async () => {
499+
setupApp({ isSSR: false })
500+
const router = getRouter()
501+
const l1 = mockedLoader({ errors: [CustomError] })
502+
router.addRoute({
503+
name: '_test',
504+
path: '/fetch',
505+
component,
506+
meta: {
507+
loaders: [l1.loader],
508+
},
509+
})
510+
511+
router.push('/fetch')
512+
await vi.runOnlyPendingTimersAsync()
513+
l1.reject(new Error('unexpected'))
514+
await expect(router.getPendingNavigation()).rejects.toThrow('unexpected')
515+
expect(router.currentRoute.value.fullPath).not.toBe('/fetch')
516+
})
517+
518+
it('works with a function check', async () => {
519+
setupApp({ isSSR: false })
520+
const router = getRouter()
521+
const l1 = mockedLoader({
522+
errors: (e) => e instanceof Error && e.message === 'expected',
523+
})
524+
router.addRoute({
525+
name: '_test',
526+
path: '/fetch',
527+
component,
528+
meta: {
529+
loaders: [l1.loader],
530+
},
531+
})
532+
533+
router.push('/fetch')
534+
await vi.runOnlyPendingTimersAsync()
535+
l1.reject(new Error('expected'))
536+
await router.getPendingNavigation()
537+
expect(router.currentRoute.value.fullPath).toBe('/fetch')
538+
539+
// use an unexpected error
540+
await router.push('/')
541+
router.push('/fetch')
542+
await vi.runOnlyPendingTimersAsync()
543+
l1.reject(new Error('unexpected'))
544+
await router.getPendingNavigation()
545+
expect(router.currentRoute.value.fullPath).not.toBe('/fetch')
546+
})
547+
})
474548
})

src/data-loaders/navigation-guard.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function setupLoaderGuard({
3636
app,
3737
effect,
3838
isSSR,
39-
errors = [],
39+
errors: globalErrors = [],
4040
selectNavigationResult = (results) => results[0]!.value,
4141
}: SetupLoaderGuardOptions) {
4242
// avoid creating the guards multiple times
@@ -151,7 +151,7 @@ export function setupLoaderGuard({
151151
setCurrentContext([])
152152
return Promise.all(
153153
loaders.map((loader) => {
154-
const { server, lazy } = loader._.options
154+
const { server, lazy, errors } = loader._.options
155155
// do not run on the server if specified
156156
if (!server && isSSR) {
157157
return
@@ -170,15 +170,26 @@ export function setupLoaderGuard({
170170
return !isSSR && toLazyValue(lazy, to, from)
171171
? undefined
172172
: // return the non-lazy loader to commit changes after all loaders are done
173-
ret.catch((reason) =>
174-
// Check if the error is an expected error to discard it
175-
loader._.options.errors?.some((Err) => reason instanceof Err) ||
176-
(Array.isArray(errors)
177-
? errors.some((Err) => reason instanceof Err)
178-
: errors(reason))
173+
ret.catch((reason) => {
174+
// use local error option if it exists first and then the global one
175+
if (
176+
errors &&
177+
(Array.isArray(errors)
178+
? errors.some((Err) => reason instanceof Err)
179+
: errors(reason))
180+
) {
181+
return // avoid any navigation failure
182+
}
183+
184+
// is the error a globally expected error
185+
return (
186+
Array.isArray(globalErrors)
187+
? globalErrors.some((Err) => reason instanceof Err)
188+
: globalErrors(reason)
189+
)
179190
? undefined
180191
: Promise.reject(reason)
181-
)
192+
})
182193
})
183194
) // let the navigation go through by returning true or void
184195
.then(() => {

0 commit comments

Comments
 (0)