T
TanStack9mo ago
foreign-sapphire

useMatchRoute but don't re-render on every state change

One thing (among many) that is awesome about tanstack/router is that most hooks can be used in a way that minimizes re-renders. For example:
const pathname = useLocation({ select: l => l.pathname }) // only re-renders when pathname changes
const foo = useSearch({ select: s => s.foo }) // only re-renders when foo changes
const router = useRouter() // never re-renders
const pathname = useLocation({ select: l => l.pathname }) // only re-renders when pathname changes
const foo = useSearch({ select: s => s.foo }) // only re-renders when foo changes
const router = useRouter() // never re-renders
But there is one pattern that I'm not really sure how to do without re-rendering on almost every router state change:
const match = useMatchRoute()
const isMatch = match({ to: '/a' }) || match({ to: '/b', fuzzy: true })
const match = useMatchRoute()
const isMatch = match({ to: '/a' }) || match({ to: '/b', fuzzy: true })
It seems to me that useMatchRoute must always re-render so that match gets re-executed (and it doesn't know what match is going to be called with). match is great because it's way more type-safe than (for example) just doing string comparisons with pathname
const pathname = useLocation({ select: l => l.pathname })
const isMatch = pathname === '/a' || pathname.startsWith('/b')
const pathname = useLocation({ select: l => l.pathname })
const isMatch = pathname === '/a' || pathname.startsWith('/b')
Or even better
const isMatch = useLocation({
select: l => l.pathname === '/a' || l.pathname.startsWith('/b')
})
const isMatch = useLocation({
select: l => l.pathname === '/a' || l.pathname.startsWith('/b')
})
This last example with useLocation causes way fewer re-renders than the equivalent with useMatchRoute, but it's not as typesafe. How would you compute isMatch in a way that is both type-safe and minimizes re-renders? There is something kind of close with useMatch but not quite the same
useMatch({ from: '/a', shouldThrow: false, select: Boolean })
useMatch({ from: '/a', shouldThrow: false, select: Boolean })
But it's missing a couple things: - fuzzy:true is pretty handy, if I want to match /foo/a and /foo/b but there is no route at /foo, fuzzy:true can do it, but useMatch cannot - the ability to specify more than just to (like search, params, ...)
2 Replies
secure-lavender
secure-lavender9mo ago
can you please provide a complete minimal example that shows what you need this for?
foreign-sapphire
foreign-sapphireOP8mo ago
ok here's an example of a menu with "messy" active links: https://stackblitz.com/edit/tanstack-router-8badpulj?file=src%2Froutes%2F__root.tsx i'm migrating a codebase with some parts that are less than optimal, so there's a bunch of places where we find some form of logic based on useLocation that is basically trying to answer "am I in this part of the app or that part of the app?". My example of a menu is just one thing that came to mind. i guess what I'm looking for can be done in user-land, but it's somewhat complicated:
const useMatchPath = <
TRouter extends AnyRouter = RegisteredRouter,
const TFrom extends string = string,
const TTo extends string | undefined = undefined,
const TMaskFrom extends string = TFrom,
const TMaskTo extends string = '',
TSelected = false | ResolveRoute<TRouter, TFrom, TTo>['types']['allParams'],
>(
opts: UseMatchRouteOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & {
select?: (match: false | ResolveRoute<TRouter, TFrom, TTo>['types']['allParams']) => TSelected
},
): TSelected => {
const router = useRouter()

return useRouterState({
select: () => {
const { pending, caseSensitive, fuzzy, includeSearch, select, ...rest } = opts
const match = router.matchRoute(rest as any, { caseSensitive, fuzzy, includeSearch, pending })
return select ? select(match) : match
},
}) as TSelected
}
const useMatchPath = <
TRouter extends AnyRouter = RegisteredRouter,
const TFrom extends string = string,
const TTo extends string | undefined = undefined,
const TMaskFrom extends string = TFrom,
const TMaskTo extends string = '',
TSelected = false | ResolveRoute<TRouter, TFrom, TTo>['types']['allParams'],
>(
opts: UseMatchRouteOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & {
select?: (match: false | ResolveRoute<TRouter, TFrom, TTo>['types']['allParams']) => TSelected
},
): TSelected => {
const router = useRouter()

return useRouterState({
select: () => {
const { pending, caseSensitive, fuzzy, includeSearch, select, ...rest } = opts
const match = router.matchRoute(rest as any, { caseSensitive, fuzzy, includeSearch, pending })
return select ? select(match) : match
},
}) as TSelected
}
use like this:
const isMatch = useMatchPath({
to: '/a',
fuzzy: true,
select: Boolean
})
const isMatch = useMatchPath({
to: '/a',
fuzzy: true,
select: Boolean
})

Did you find this page helpful?