Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/services/createExternalRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function createExternalRoute(options: CreateRouteOptions & (WithoutHost |
const meta = options.meta ?? {}
const host = toWithParams(options.host)
const context = options.context ?? []
const { store, onBeforeRouteEnter } = createHooksFactory()
const { store, onBeforeRouteEnter, onError } = createHooksFactory()
const rawRoute = markRaw({ id, meta: {}, state: {}, ...options })

const route = {
Expand All @@ -45,6 +45,7 @@ export function createExternalRoute(options: CreateRouteOptions & (WithoutHost |
state: {},
context,
onBeforeRouteEnter,
onError,
} satisfies Route & ExternalRouteHooks

const merged = isWithParent(options) ? combineRoutes(options.parent, route) : route
Expand Down
33 changes: 27 additions & 6 deletions src/services/getRouteHooks.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
import { ResolvedRoute } from '@/types/resolved'
import { isRouteEnter, isRouteLeave, isRouteUpdate } from '@/services/hooks'
import { Hooks } from '@/models/hooks'
import { AfterHook, BeforeHook } from '@/types/hooks'

function wrapHookCatch(route: ResolvedRoute | null): <T extends BeforeHook | AfterHook>(hook: T) => T {
const hooks = route?.hooks.flatMap((store) => Array.from(store.onError)) ?? []

if (!hooks.length) {
return (hook) => hook
}

return <T extends BeforeHook | AfterHook>(hook: T): T => ((to: any, context: any) => {
try {
return hook(to, context)
} catch (error) {
hooks.forEach((runErrorHook) => runErrorHook(error, { ...context, to, source: 'hook' }))
}
}) as T
}

export function getBeforeHooksFromRoutes(to: ResolvedRoute, from: ResolvedRoute | null): Hooks {
const hooks = new Hooks()
const toErrorWrapper = wrapHookCatch(to)
const fromErrorWrapper = wrapHookCatch(from)

to.hooks.forEach((store, depth) => {
if (isRouteEnter(to, from, depth)) {
return store.onBeforeRouteEnter.forEach((hook) => hooks.onBeforeRouteEnter.add(hook))
return store.onBeforeRouteEnter.forEach((hook) => hooks.onBeforeRouteEnter.add(toErrorWrapper(hook)))
}

if (isRouteUpdate(to, from, depth)) {
return store.onBeforeRouteUpdate.forEach((hook) => hooks.onBeforeRouteUpdate.add(hook))
return store.onBeforeRouteUpdate.forEach((hook) => hooks.onBeforeRouteUpdate.add(toErrorWrapper(hook)))
}
})

from?.hooks.forEach((store, depth) => {
if (isRouteLeave(to, from, depth)) {
return store.onBeforeRouteLeave.forEach((hook) => hooks.onBeforeRouteLeave.add(hook))
return store.onBeforeRouteLeave.forEach((hook) => hooks.onBeforeRouteLeave.add(fromErrorWrapper(hook)))
}
})

Expand All @@ -26,20 +45,22 @@ export function getBeforeHooksFromRoutes(to: ResolvedRoute, from: ResolvedRoute

export function getAfterHooksFromRoutes(to: ResolvedRoute, from: ResolvedRoute | null): Hooks {
const hooks = new Hooks()
const toErrorWrapper = wrapHookCatch(to)
const fromErrorWrapper = wrapHookCatch(from)

to.hooks.forEach((store, depth) => {
if (isRouteEnter(to, from, depth)) {
return store.onAfterRouteEnter.forEach((hook) => hooks.onAfterRouteEnter.add(hook))
return store.onAfterRouteEnter.forEach((hook) => hooks.onAfterRouteEnter.add(toErrorWrapper(hook)))
}

if (isRouteUpdate(to, from, depth)) {
return store.onAfterRouteUpdate.forEach((hook) => hooks.onAfterRouteUpdate.add(hook))
return store.onAfterRouteUpdate.forEach((hook) => hooks.onAfterRouteUpdate.add(toErrorWrapper(hook)))
}
})

from?.hooks.forEach((store, depth) => {
if (isRouteLeave(to, from, depth)) {
return store.onAfterRouteLeave.forEach((hook) => hooks.onAfterRouteLeave.add(hook))
return store.onAfterRouteLeave.forEach((hook) => hooks.onAfterRouteLeave.add(fromErrorWrapper(hook)))
}
})

Expand Down
49 changes: 49 additions & 0 deletions src/services/hooks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,52 @@ test('when onError callback calls replace, other onError callbacks do not run',
expect(errorHook2).not.toHaveBeenCalled()
expect(errorHook3).not.toHaveBeenCalled()
})

test('onError hooks on routes only called when that route hooks error', () => {
const errorHook = vi.fn()

const routeWithErrorHook = createRoute({
name: 'route-with-hook',
})

const routeWithoutErrorInHook = createRoute({
name: 'route-without-hook',
})

routeWithErrorHook.onBeforeRouteEnter(() => {
throw new Error('Test error')
})

routeWithErrorHook.onBeforeRouteLeave(() => {
throw new Error('Test error')
})

routeWithoutErrorInHook.onBeforeRouteEnter(() => {
throw new Error('Test error')
})

routeWithErrorHook.onError(() => errorHook())

const { runBeforeRouteHooks } = createRouterHooks()

// neither route has error hook
runBeforeRouteHooks({
to: createResolvedRoute(routeWithoutErrorInHook, {}),
from: null,
})
expect(errorHook).not.toHaveBeenCalled()

// to route has error hook
runBeforeRouteHooks({
to: createResolvedRoute(routeWithErrorHook, {}),
from: null,
})
expect(errorHook).toHaveBeenCalledTimes(1)

// from route has error hook
runBeforeRouteHooks({
to: createResolvedRoute(createRoute({ name: 'random-route' }), {}),
from: createResolvedRoute(routeWithErrorHook, {}),
})
expect(errorHook).toHaveBeenCalledTimes(2)
})
10 changes: 10 additions & 0 deletions src/types/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,23 @@ export type InternalRouteHooks<TContext extends RouteContext[] | undefined = und
* Registers a route hook to be called after the route is updated.
*/
onAfterRouteUpdate: AddAfterHook<RouteContextToRoute<TContext>, RouteContextToRejection<TContext>>,
/**
* Registers a hook to be called when an error occurs.
* If the hook returns true, the error is considered handled and the other hooks are not run. If all hooks return false the error is rethrown
*/
onError: AddErrorHook<RouteContextToRoute<TContext>, RouteContextToRejection<TContext>>,
}

export type ExternalRouteHooks<TContext extends RouteContext[] | undefined = undefined> = {
/**
* Registers a route hook to be called before the route is entered.
*/
onBeforeRouteEnter: AddBeforeHook<RouteContextToRoute<TContext>, RouteContextToRejection<TContext>>,
/**
* Registers a hook to be called when an error occurs.
* If the hook returns true, the error is considered handled and the other hooks are not run. If all hooks return false the error is rethrown
*/
onError: AddErrorHook<RouteContextToRoute<TContext>, RouteContextToRejection<TContext>>,
}

export type HookTiming = 'global' | 'component'
Expand Down