named-urls
named-urls copied to clipboard
New API using closures
Instead of reverse(path, { params }) what about simply call path({ params })?
Consider following routes:
// routes.js
export default {
// simple route
profile: '/profile',
// route with params
article: '/article/:articleId',
}
Each named route is a function, which accepts route params:
import routes from './routes'
const ProfilePage() {
return (
<div>
<h1>My articles</h1>
{articles.map(article => (
<Link to={routes.article({ articleId: article.id })}>{article.name}</Link>
)}
</div>
)
}
Also, each route also has a path attribute, which returns raw pattern:
import routes from './routes'
const App() {
return (
<Switch>
<Route path={routes.profile.path} component={ProfilePage} />
<Route path={routes.article.path} component={ArticlePage} />
</Switch>
)
}
Advantages:
- It simplifies the API a lot. Instead of importing
reverseandroutes, is enough to import simplyroutes. I don't think there's any overhead, because routes are constructed only once when the app is loaded. - Right now, nested routes (created using
include) must usetoString()on root route, otherwise object of child routes is returned. Havingpathattribute would unify the API.
Disadvantages:
pathattribute might clash with a route namedpathin child routes
What do you think @almaron?
What about included routes?
It's the same:
// routes.js
export default {
auth: include('/auth', {
login: '/login',
reset: '/reset/:token'
})
}
Using pattern:
<Route path={routes.auth.path} /> // used to be `routes.auth.toString()`
<Route path={routes.auth.reset.path} />
Reversing:
<Link to={routes.auth()} />
<Link to={routes.auth.reset({ token: 'xyz' }) />
I like this idea. But maybe there should be one additional piece:
Routes as functions:
export default {
auth: include('/auth', {
login: '/login',
reset: (token) => { return `/reset/${token}`; }
})
}
It's of course not useful for such simple scenarios, but imagine you have recursive routes and / or array parameters and / or object parameters. Something like:
export default {
children: include('/children', {
firstChild: '/:id',
multiple: (ids) => { return `/${ids.join('/')}`; }, // will result in /children/4/3/1/2/...
complexParam: (fooBar) => { return `/${fooBar.str}/${fooBar.bool ? 'foo' : 'bar'}` }
})
}
And of course there must be a way to set the :param stuff for react router:
export default {
auth: include('/auth', {
login: '/login',
reset: (token = ":token") => { return `/reset/${token}`; }
})
}
Now when you call reset() it will result in /reset/:token and reset('foo') will result in /reset/foo
Interesting idea!
How would you provide :params for react router for your example?
export default {
children: include('/children', {
firstChild: '/:id',
multiple: (ids) => { return `/${ids.join('/')}`; }, // will result in /children/4/3/1/2/...
complexParam: (fooBar) => { return `/${fooBar.str}/${fooBar.bool ? 'foo' : 'bar'}` }
})
}
The main reason behind this idea was, that I'm using TypeScript. And I like to have my IDE telling me what's allowed or not. And the easiest way is having function arguments :)
In the last example I showed a simple usage for :params using "default arguments":
foo: include('/foo',
bar: (baz = ':baz') => { return `/${baz}` }
)
This function will return /:baz if you provide no arguments (= calling bar()). And else it will use the provided argument. So you would use something like <Route path={Routes.foo.bar()} /> and <Link to={Routes.foo.bar("123")} />.
And otherwise: It's a function, you can do whatever you want:
include('/foo',
bar: (baz) => {
if(baz === undefined) return "/:whateveryouwant";
else if(typeof baz === "number") return "/"+(baz+42);
else return `/${baz}`;
}
)
But now I came across another possible issue with this... It may be necessary (or "useful") to have access to react router's match.url. Especially for recursive routes (see: https://reacttraining.com/react-router/web/example/recursive-paths ). I'm not sure if that's even possible to inject the match.url.
EDIT: I now built a simplified version of your code, which handles only functions. That was pretty straight forward. The only downside is that for every level of nesting the function chain gets longer and longer.
const include = (base, routes) => {
return Object.entries(routes).reduce((agg, [route, url]) => {
if(typeof url === "function") {
// here's the function chaining
agg[route] = (...args) => base + url.apply(null, args);
} else if(typeof url === "object") {
agg[route] = include(base, url);
}
return agg;
}, {});
}
Test code:
const routes = include('/foo', {
root: () => '',
bar: include('/bar', {
baz: () => '/baz',
withParam: (param = ':param') => "/"+param
})
});
console.log(routes.root()) // result: "/foo" (calls 1 function)
console.log(routes.bar.baz()) // result: "/foo/bar/baz" (calls 2 chained functions)
console.log(routes.bar.withParam()) // result: "/foo/bar/:param" (calls 2 chained functions)
console.log(routes.bar.withParam("123")) // result: "/foo/bar/123" (calls 2 chained functions)
In terms of React Router it works like this:
<Route path={routes.bar.withParam()} />
<Link to={routes.bar.withParams("123")} />
Maybe the chained functions can be optimized away somehow. But so far it works great!
Side note - it's beginning to get way more complicated...