react-chicane

mit licence
npm version
bundlephobia

A simple and safe router for React and TypeScript.

Installation

yarn add react-chicane

Run the example

git clone [email protected]:zoontek/react-chicane.git
cd ./react-chicane/example
yarn install && yarn dev

πŸ“˜ Usage

Creating a router

This library exports only one function: createRouter. The goal behind this is to enforce listing all your project routes using fancy names in a file and use the strongly typed methods returned.

import { createRouter } from "react-chicane";

const { useRoute } = createRouter({
  root: "/",
  users: "/users",
  user: "/users/:userId",
});

const App = () => {
  const route = useRoute(["root", "users", "user"]);

  if (!route) {
    return <h1>404</h1>;
  }

  // route object is a discriminated union
  switch (route.name) {
    case "root":
      return <h1>Homepage</h1>;
    case "users":
      return <h1>Users</h1>;
    case "user":
      // params are strongly typed
      return <h1>User {route.params.userId}</h1>;
  }
};

πŸ‘‰ Note: I strongly recommand using a pattern matching library, like the excellent ts-pattern.

✍️ Path syntax

react-chicane doesn’t bother about what’s inside your path, your search params or your hash. It only exposes an object, params.

  • A param in your path will result in a required string
  • A param in your search or your hash will result in an optional string
  • A mutiple param in your search will result in a optional string[]

import { createRouter } from "react-chicane";

export const { useRoute } = createRouter({
  groups: "/groups",
  group: "/groups/:groupId?:foo&:bar[]#:baz",
  users: "/groups/:groupId/users",
  user: "/groups/:groupId/users/:userId",
  // it also supports wildcard routes!
  "users*": "/groups/:groupId/users*",
});

const App = () => {
  const route = useRoute(["groups", "group", "users", "user"]);

  if (!route) {
    return <h1>404</h1>;
  }

  switch (route.name) {
    case "groups":
      return route.params; // {}
    case "group":
      return route.params; // { groupId: string, foo?: string, bar?: string[], baz?: string }
    case "users":
      return route.params; // { groupId: string }
    case "user":
      return route.params; // { groupId: string, userId: string }
  }
};

πŸ‘‰ Note: Non-param search and hash are not supported.

πŸ”— Creating URLs

Because it’s also nice to create safe internal URLs, createRouter also returns createURL.

import { createRouter } from "react-chicane";

const { createURL } = createRouter({
  root: "/",
  users: "/users",
  user: "/users/:userId",
});

createURL("root"); // -> "/"
createURL("users"); // -> "/users"
createURL("user", { userId: "zoontek" }); // -> "/users/zoontek"

βš™οΈ API

createRouter

Create a router instance for your whole application.

import { createRouter } from "react-chicane";

const Router = createRouter(
  {
    root: "/",
    users: "/users",
    user: "/users/:userId",
  },
  // { basePath: "/setup/basePath/here" }
);

πŸ‘‡ Note: All the following examples will use this Router instance.

Router.location

type Location = {
  path: string[];
  search: Record<string, string | string[]>;
  hash?: string;
};

Router.location; // Location

Router.url

Router.url; // string

Router.navigate

Navigate to a given route.

Router.navigate("root");
Router.navigate("users");
Router.navigate("user", { userId: "zoontek" });

Router.replace

Same as navigate, but will replace the current route in the browser history.

Router.replace("root");
Router.replace("users");
Router.replace("user", { userId: "zoontek" });

Router.goBack

Go back in browser history.

Router.goBack();

Router.goForward

Go forward in browser history.

Router.goForward();

Router.createURL

Safely create internal URLs.

createURL("root"); // -> "/"
createURL("users"); // -> "/users"
createURL("user", { userId: "zoontek" }); // -> "/users/zoontek"

Router.useRoute

Listen and match a bunch of your routes. Awesome with pattern matching.

import { match } from "ts-pattern";

const App = () => {
  // The order isn't important, paths are ranked using https://reach.tech/router/ranking
  const route = Router.useRoute(["root", "users", "user"]);

  match(route)
    .with({ name: "root" }, () => null)
    .with({ name: "users" }, () => null)
    .with({ name: "user" }, ({ params } /* { groupId: string } */) => null)
    .otherwise(() => <h1>404</h1>);
};

Router.useLink

As this library doesn’t provide a single component, we expose this hook to create your own customized Link.

const Link = ({
  children,
  href,
  replace,
  target,
}: {
  children?: React.ReactNode;
  href: string;
  replace?: boolean;
  target?: React.HTMLAttributeAnchorTarget;
}) => {
  const { active, onClick } = Router.useLink({ href, replace, target });

  return (
    <a
      href={href}
      target={target}
      onClick={onClick}
      style={{ fontWeight: active ? 700 : 400 }}
    >
      {children}
    </a>
  );
};

// usage
<Link href={Router.createURL("user", { userId: "zoontek" })} />;

Router.useLocation

Listen and react on Router.location changes.

const App = () => {
  const location: Location = Router.useLocation();

  React.useEffect(() => {
    console.log("location changed", location);
  }, [location]);

  /* … */
};

Router.useURL

Listen and react on Router.url changes.

const App = () => {
  const url: string = Router.useURL();

  React.useEffect(() => {
    console.log("url changed", url);
  }, [url]);

  /* … */
};

Router.subscribe

Subscribe to location changes. Useful to reset keyboard focus.

const App = () => {
  React.useEffect(() => {
    const unsubscribe = Router.subscribe((location) => {
      resetKeyboardFocusToContent();
    });

    return unsubscribe;
  }, []);

  /* … */
};

πŸ‘·β€β™‚οΈ Roadmap

  • Improve documentation
  • Tests, tests, tests
  • Switch to useMutableSource (React 18+)
  • Add navigation blocker
  • Write a “focus reset” recipe
  • Find a cool logo
  • Create a website (?)

πŸ™Œ Acknowledgements

GitHub

View Github