Skip to content

Commit

Permalink
feat(loaders): allow errors as a function
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Sep 8, 2024
1 parent 646e5bf commit df80b28
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 11 deletions.
4 changes: 2 additions & 2 deletions src/data-loaders/createDataLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ export interface DefineDataLoaderOptionsBase<isLazy extends boolean> {

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

export const toLazyValue = (
Expand Down
2 changes: 2 additions & 0 deletions src/data-loaders/defineLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ export function defineBasicLoader<Data, isLazy extends boolean>(
): Promise<void> {
const entries = router[LOADER_ENTRIES_KEY]!
const isSSR = router[IS_SSR_KEY]

// ensure the entry exists
if (!entries.has(loader)) {
entries.set(loader, {
// force the type to match
Expand Down
74 changes: 74 additions & 0 deletions src/data-loaders/navigation-guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,4 +471,78 @@ describe('navigation-guard', () => {
expect(router.currentRoute.value.fullPath).toBe('/#ok')
})
})

describe('errors', () => {
class CustomError extends Error {}

it('lets the navigation continue if the error is expected', async () => {
setupApp({ isSSR: false })
const router = getRouter()
const l1 = mockedLoader({ errors: [CustomError] })
router.addRoute({
name: '_test',
path: '/fetch',
component,
meta: {
loaders: [l1.loader],
},
})

router.push('/fetch')
await vi.runOnlyPendingTimersAsync()
l1.reject(new CustomError('expected'))
await router.getPendingNavigation()
expect(router.currentRoute.value.fullPath).toBe('/fetch')
})

it('fails the navigation if the error is not expected', async () => {
setupApp({ isSSR: false })
const router = getRouter()
const l1 = mockedLoader({ errors: [CustomError] })
router.addRoute({
name: '_test',
path: '/fetch',
component,
meta: {
loaders: [l1.loader],
},
})

router.push('/fetch')
await vi.runOnlyPendingTimersAsync()
l1.reject(new Error('unexpected'))
await expect(router.getPendingNavigation()).rejects.toThrow('unexpected')
expect(router.currentRoute.value.fullPath).not.toBe('/fetch')
})

it('works with a function check', async () => {
setupApp({ isSSR: false })
const router = getRouter()
const l1 = mockedLoader({
errors: (e) => e instanceof Error && e.message === 'expected',
})
router.addRoute({
name: '_test',
path: '/fetch',
component,
meta: {
loaders: [l1.loader],
},
})

router.push('/fetch')
await vi.runOnlyPendingTimersAsync()
l1.reject(new Error('expected'))
await router.getPendingNavigation()
expect(router.currentRoute.value.fullPath).toBe('/fetch')

// use an unexpected error
await router.push('/')
router.push('/fetch')
await vi.runOnlyPendingTimersAsync()
l1.reject(new Error('unexpected'))
await router.getPendingNavigation()
expect(router.currentRoute.value.fullPath).not.toBe('/fetch')
})
})
})
29 changes: 20 additions & 9 deletions src/data-loaders/navigation-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function setupLoaderGuard({
app,
effect,
isSSR,
errors = [],
errors: globalErrors = [],
selectNavigationResult = (results) => results[0]!.value,
}: SetupLoaderGuardOptions) {
// avoid creating the guards multiple times
Expand Down Expand Up @@ -151,7 +151,7 @@ export function setupLoaderGuard({
setCurrentContext([])
return Promise.all(
loaders.map((loader) => {
const { server, lazy } = loader._.options
const { server, lazy, errors } = loader._.options
// do not run on the server if specified
if (!server && isSSR) {
return
Expand All @@ -170,15 +170,26 @@ export function setupLoaderGuard({
return !isSSR && toLazyValue(lazy, to, from)
? undefined
: // return the non-lazy loader to commit changes after all loaders are done
ret.catch((reason) =>
// Check if the error is an expected error to discard it
loader._.options.errors?.some((Err) => reason instanceof Err) ||
(Array.isArray(errors)
? errors.some((Err) => reason instanceof Err)
: errors(reason))
ret.catch((reason) => {
// use local error option if it exists first and then the global one
if (
errors &&
(Array.isArray(errors)
? errors.some((Err) => reason instanceof Err)
: errors(reason))
) {
return // avoid any navigation failure
}

// is the error a globally expected error
return (
Array.isArray(globalErrors)
? globalErrors.some((Err) => reason instanceof Err)
: globalErrors(reason)
)
? undefined
: Promise.reject(reason)
)
})
})
) // let the navigation go through by returning true or void
.then(() => {
Expand Down

0 comments on commit df80b28

Please sign in to comment.