router
router copied to clipboard
Different dynamic route loading with "*" not possible (loader output is cached)
Describe the bug
Sometimes it may be desired/required to have routes defined outside of react-router or the javascript app or they are just not all known to the app. For example asking the backend if it has something to serve for this route.
One of my usecases is content served from an API with thousands of routes, some of which are only available to certain users and deciding which user can access what route is done on-the-fly by the backend.
This use-case can be served by adding a route like this (where fetchRoute performs a request to the backend):
{
path: "*",
loader: ({pathname}) => (
queryClient.getQueryData(["route", pathname]) ?? queryClient.fetchQuery(["route", pathname], () => fetchRoute(pathname))
),
element: <RouteComponent />,
}
Unfortunately, a "*" route currently has the same cache key regardless of the pathname, and therefore will fetch the correct content the first route and then return the same content for all following routes.
(Using a route parameter ":foobar" will work for top-level routes, but does not cover n levels of nested routes.)
To Reproduce I don't think an example is necessary right now, but I'd be happy to create one if needed.
Expected behavior
I think we should consider both the route path "*" aswell as the matched pathname for the cache key and re-run the loader for every pathname.
Workaround My currently working workaround:
const loader = React.useCallback(({pathname}) => (
queryClient.getQueryData(["route", pathname]) ?? queryClient.fetchQuery(["route", pathname], () => fetchRoute(pathname))
), [])
const routes = React.useMemo(() => {
const _routes = []
const routePaths = ["/"]
let lastPath = ""
Array(26).fill().map((element, index) => String.fromCharCode("a".charCodeAt(0) + index)).map(char => {
lastPath = `${lastPath}/:${char}`
routePaths.push(`${lastPath}/`)
})
routePaths.reverse().forEach((path) => {
_routes.push({
loader,
element: <RouteComponent />,
path,
})
})
return _routes
}, [])
This will produce the following 27 routes, all with the same loader and element:
//:a//:a/:b//:a/:b/:c/- ...
/:a/:b/:c/:d/:e/:f/:g/:h/:i/:j/:k/:l/:m/:n/:o/:p/:q/:r/:s/:t/:u/:v/:w/:x/:y/:z/
There might be an API that you can use to just watch the route match wildcard in an effect and re-trigger the loader when it changes. Would that work?
Yeah, I think that could work too. As long as we have some method to invalidate the cache. I also did some more thinking, so I see 3 possible solutions:
-
your idea
-
per route cacheKey callback Another idea is (I think I saw that on some other react-routing lib or something): Adding a callback to calculate the cache key. Right now a route with
path: "*"probably has the cache key"*". What if we make a callback like this:{ path: "*", loader: ({pathname}) => ( queryClient.getQueryData(["route", pathname]) ?? queryClient.fetchQuery(["route", pathname], () => fetchRoute(pathname)) ), //cacheKey: (path, match) => path, // DEFAULT behavior, so it returns `*` cacheKey: (path, match) => `${path}|${match.pathname}`, // my desired behavior, so it returns `*|/foo/bar/baz` or whatever element: <RouteComponent />, } -
allow a callback for
path(likesearchworks?) How about makingpatha callback that can do it's own matching and return route parameters (like:a/:bwould). This way people could also do regex stuff (which would address #77 ). Kind of like how I thinksearchworks. Here's an example that would cover the usecase from #77 (/:segment(a|b|c)/:id(\\d+)):{ path: (locationMatch) => { const regexMatch = locationMatch.pathname.match(/\/(a|b|c)/(\d+)/?/) if (!regexMatch) return false; return { segment: regexMatch[1], // a, b, or c id: regexMatch[2], // number } }, loader: ({pathname}) => ( queryClient.getQueryData(["route", pathname]) ?? queryClient.fetchQuery(["route", pathname], () => fetchRoute(pathname)) ), element: <RouteComponent />, }
Right now, I think I like (3) the best. But any would be fine for me.
This could be a cool addition. Would you like to explore adding it?
I surely am willing to create a PR, but I doubt I'll have time in the next few weeks, so might take a while.
Facing same problem here.
Having a catch-all route is a very common pattern.
Would be great having some control over the cache key for path '*', or allowing providing a regex for the route param, Ã la express:
{
path: ':slug(.*)',
}
Also tried with this, but didn't work:
import { ReactLocationSimpleCache } from '@tanstack/react-location-simple-cache'
...
const routeCache = new ReactLocationSimpleCache()
const routes=[
{
path: '*',
loader: routeCache.createLoader(
async (match: any) => ({
data: `Definition for page ${match.params['*']}`,
}),
{
key: (match) => match.params['*'] || '',
},
),
element: async () => import('./components/Default').then(module => <module.default />),
},
]
This is both stale and fix in the latest Router version.