Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions .changeset/route-scoped-use-match-should-throw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/react-router': patch
---

Fix route-scoped useMatch APIs to forward the shouldThrow option.
1 change: 1 addition & 0 deletions packages/react-router/src/fileRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export class LazyRoute<TRoute extends AnyRoute> {
return useMatch({
select: opts?.select,
from: this.options.id,
shouldThrow: opts?.shouldThrow,
structuralSharing: opts?.structuralSharing,
} as any) as any
}
Expand Down
3 changes: 3 additions & 0 deletions packages/react-router/src/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export class RouteApi<
return useMatch({
select: opts?.select,
from: this.id,
shouldThrow: opts?.shouldThrow,
structuralSharing: opts?.structuralSharing,
} as any) as any
}
Expand Down Expand Up @@ -264,6 +265,7 @@ export class Route<
return useMatch({
select: opts?.select,
from: this.id,
shouldThrow: opts?.shouldThrow,
structuralSharing: opts?.structuralSharing,
} as any) as any
}
Expand Down Expand Up @@ -535,6 +537,7 @@ export class RootRoute<
return useMatch({
select: opts?.select,
from: this.id,
shouldThrow: opts?.shouldThrow,
structuralSharing: opts?.structuralSharing,
} as any) as any
}
Expand Down
5 changes: 3 additions & 2 deletions packages/react-router/src/useMatch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,18 @@ export type UseMatchRoute<out TFrom> = <
TRouter extends AnyRouter = RegisteredRouter,
TSelected = unknown,
TStructuralSharing extends boolean = boolean,
TThrow extends boolean = true,
>(
opts?: UseMatchBaseOptions<
TRouter,
TFrom,
true,
true,
TThrow,
TSelected,
TStructuralSharing
> &
StructuralSharingOption<TRouter, TSelected, TStructuralSharing>,
) => UseMatchResult<TRouter, TFrom, true, TSelected>
) => ThrowOrOptional<UseMatchResult<TRouter, TFrom, true, TSelected>, TThrow>

export type UseMatchOptions<
TRouter extends AnyRouter,
Expand Down
19 changes: 19 additions & 0 deletions packages/react-router/tests/routeApi.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,25 @@ describe('getRouteApi', () => {
expectTypeOf(invoiceRouteApi.useMatch<DefaultRouter>()).toEqualTypeOf<
MakeRouteMatch<typeof routeTree, '/invoices/$invoiceId'>
>()

expectTypeOf(
invoiceRouteApi.useMatch<DefaultRouter>({ shouldThrow: true }),
).toEqualTypeOf<MakeRouteMatch<typeof routeTree, '/invoices/$invoiceId'>>()

expectTypeOf(
invoiceRouteApi.useMatch<DefaultRouter, unknown, boolean, false>({
shouldThrow: false,
}),
).toEqualTypeOf<
MakeRouteMatch<typeof routeTree, '/invoices/$invoiceId'> | undefined
>()

expectTypeOf(
invoiceRouteApi.useMatch<DefaultRouter, string, boolean, false>({
shouldThrow: false,
select: (match) => match.params.invoiceId,
}),
).toEqualTypeOf<string | undefined>()
})
test('Link', () => {
const Link = invoiceRouteApi.Link
Expand Down
117 changes: 117 additions & 0 deletions packages/react-router/tests/useMatch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
createRootRoute,
createRoute,
createRouter,
getRouteApi,
useMatch,
} from '../src'
import type { RouteComponent, RouterHistory } from '../src'
Expand Down Expand Up @@ -288,3 +289,119 @@ describe('useMatch', () => {
})
})
})

describe('Route.useMatch', () => {
function createPostsRouter(
RootComponent: RouteComponent,
history?: RouterHistory,
) {
const rootRoute = createRootRoute({
component: RootComponent,
})
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => <h1>IndexTitle</h1>,
})
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
component: () => <h1>PostsTitle</h1>,
})
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
history: history ?? createMemoryHistory({ initialEntries: ['/'] }),
})

return { postsRoute, router }
}

type PostsRoute = ReturnType<typeof createPostsRouter>['postsRoute']

test('returns undefined when the target route is inactive and shouldThrow is false', async () => {
const route = {} as { current: PostsRoute }

function RootComponent() {
const match = route.current.useMatch({ shouldThrow: false })
expect(match).toBeUndefined()
return <Outlet />
}

const created = createPostsRouter(RootComponent)
route.current = created.postsRoute
render(<RouterProvider router={created.router} />)
expect(await screen.findByText('IndexTitle')).toBeInTheDocument()
})

test.each([undefined, true])(
'throws when the target route is inactive and shouldThrow is %s',
async (shouldThrow) => {
const route = {} as { current: PostsRoute }

function RootComponent() {
route.current.useMatch({ shouldThrow })
return <Outlet />
}

const created = createPostsRouter(RootComponent)
route.current = created.postsRoute
render(<RouterProvider router={created.router} />)
const postsError = await screen.findByText(
'Invariant failed: Could not find an active match from "/posts"',
)
expect(postsError).toBeInTheDocument()
},
)

test('returns the match when the target route is active and shouldThrow is false', async () => {
const route = {} as { current: PostsRoute }

function RootComponent() {
const match = route.current.useMatch({ shouldThrow: false })
expect(match).toBeDefined()
expect(match?.routeId).toBe('/posts')
return <Outlet />
}

const created = createPostsRouter(
RootComponent,
createMemoryHistory({ initialEntries: ['/posts'] }),
)
route.current = created.postsRoute
render(<RouterProvider router={created.router} />)
expect(await screen.findByText('PostsTitle')).toBeInTheDocument()
})
})

describe('RouteApi.useMatch', () => {
test('returns undefined when the target route is inactive and shouldThrow is false', async () => {
const postsRouteApi = getRouteApi('/posts')

function RootComponent() {
const match = postsRouteApi.useMatch({ shouldThrow: false })
expect(match).toBeUndefined()
return <Outlet />
}

const rootRoute = createRootRoute({
component: RootComponent,
})
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => <h1>IndexTitle</h1>,
})
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
component: () => <h1>PostsTitle</h1>,
})
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
history: createMemoryHistory({ initialEntries: ['/'] }),
})

render(<RouterProvider router={router} />)
expect(await screen.findByText('IndexTitle')).toBeInTheDocument()
})
})
Loading