swan-io/chicane

Simpler type to represent a valid route object

Closed this issue · 4 comments

Why it is needed?

At the moment if you want to create a function or component that receives a route name and its corresponding parameters, there doesn't seem to be a suitable type exported by the package.

For instance, there doesn't seem to be a simple Route type available:

function doSomething(route: Route) {
  Router.push(route)
}

This seems to be a very common requirement for building UI components like navs/menus/tabs, etc - where you need to be able to pass in a valid route object that can be inspected (for determining whether a nav item is "active") and to pass to Router.push() if required.

Possible implementation

I appreciate this would at least need to take a parameter of the route definitions, e.g. Route<typeof routes>.

Code sample

No response

Hi 👋

Having a generic type would defeat the goal of type safety. This is easily doable using this:

import { createRouter } from "@swan-io/chicane";

export const Router = createRouter({
  Home: "/",
  Users: "/users",
  User: "/users/:userId",
  RepositoriesArea: "/users/:userId/repositories/*",
  Repositories: "/users/:userId/repositories",
  Repository: "/users/:userId/repositories/:repositoryId",
});

// a discriminated union for all your routes (not generic), typed
export type Route = NonNullable<ReturnType<typeof Router.useRoute>>;

If you want to be able to pass any route, with or without params, I recommend a simple:

function doSomething(url: string) {
  pushUnsafe(route)
}

// …

doSomething(Routes.User({}))

Hi @zoontek - really appreciate the fast response - loving Chicane so far.

Re. the original request - I'd definitely still want the type safety of knowing that the parameters were valid for the intended route.

In pseudo code, this would seem to be a common pattern for nav components, where we both need to be able to navigate to the route if required, as well as determine whether it is the current route (ignoring perhaps query string params that could change due to interactions on that screen):

const routes = {
   Users: "/users",
   User: "/users/:id",
}
const Router = createRouter(routes)

function NavItem({[ routeName, routeArgs], text }: { route: Route<typeof routes>, text: string) {
  const isActive = !!Router.getRoute([routeName])
  return <li className={isActive ? 'active' : ''}>
    <a onClick={() => Router.push(routeName, routeArgs)}>{ text }</a>
  </li>
}

<NavItem route={['Users', {}]} text='Users' />

<NavItem route={['User', { id: 4 }]} text='User 4' />

I tried using your example of using the return type of useRoute, but that raises it's own problems - it doesn't ignore 'area' routes, and forces a rather awkward syntax for passing the route:

route={{
  key: "some",
  name: "User",
  params: { id: 4 },
}}

Maybe we have to just pass around strings, as mentioned, although it's a bit of shame if we have to parse the URL ourselves to determine whether a route is 'active'.

@coatesap I would not recommend this pattern, scope the routing match / let the parent decide if it's active or not, this way your component stays pure and does not need an history context. Less burden to maintain / exposes in storybook etc.

Your use case also match useLinkProps: https://swan-io.github.io/chicane/linking-to-a-route#creating-your-own-link-component

But if you want to design it that way, it's doable like this:

const Router = createRouter({
  Users: "/users",
  User: "/users/:id",
});

function NavItem({
  route,
  text,
}: {
  route: Parameters<typeof Router.push>;
  text: string;
}) {
  const isActive = !!Router.getRoute([route[0]]);

  return (
    <li className={isActive ? "active" : ""}>
      <a onClick={() => Router.push.apply(null, route)}>{text}</a>
    </li>
  );
}

const a = <NavItem route={["Users"]} text="Users" />;
const b = <NavItem route={["User", { id: String(4) }]} text="User 4" />;

It's normal that you cannot create url to area routes, as those are glob and not precise urls.