Skip to content
main
Switch branches/tags
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
 
 
 
 
src
 
 
 
 
 
 
 
 
 
 
 
 

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 git@github.com:zoontek/react-chicane.git
cd react-chicane/example
yarn install && yarn dev

📘 Usage

Creating a router

This library exports a main 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: Even if you can use classic type guards (if, switch, etc.) to check the result, I strongly recommand using a pattern matching library, like the excellent ts-pattern (all the following examples will).

✍️ 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";
import { match } from "ts-pattern";

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!
  usersArea: "/groups/:groupId/users/*",
});

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

  match(route)
    .with({ name: "groups" }, ({ params }) => console.log(params)) // {}
    .with({ name: "group" }, ({ params }) => console.log(params)) // { groupId: string, foo?: string, bar?: string[], baz?: string }
    .with({ name: "users" }, ({ params }) => console.log(params)) // { groupId: string }
    .with({ name: "user" }, ({ params }) => console.log(params)) // { groupId: string, userId: string }
    .otherwise(() => <h1>404</h1>);

  // …
};

👉 Note: Non-param search and hash are not supported.

🔗 Creating URLs

Because it's 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", // Will be prepend to all your paths (optional)
    blockerMessage: "Are you sure you want to leave this page?", // A default navigation blocker message (optional)
  },
);

👇 Note: All the following examples will use this Router instance.

Router.location

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

Router.location; // Location

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.

Router.createURL("root"); // -> "/"
Router.createURL("users"); // -> "/users"
Router.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"]);

  return match(route)
    .with({ name: "root" }, () => <h1>root</h1>)
    .with({ name: "users" }, () => <h1>users</h1>)
    .with({ name: "user" }, ({ params: { groupId } }) => <h1>user</h1>)
    .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,
  to,
  replace,
  target,
}: {
  children?: React.ReactNode;
  to: string;
  replace?: boolean;
  target?: React.HTMLAttributeAnchorTarget;
}) => {
  const { active, onClick } = useLink({ href: to, replace, target });

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

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

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.useBlocker

Block the navigation and ask user for confirmation. Useful to avoid loosing a form state. It accepts a second paramater if you want to override the default blockerMessage.

const App = () => {
  const { formStatus } = useForm(/* … */);

  Router.useBlocker(
    formStatus === "editing",
    "Are you sure you want to stop editing this profile?",
  );

  // …
};

Router.subscribe

Subscribe to location changes. Useful to reset keyboard focus.

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

    return unsubscribe;
  }, []);

  // …
};

Router.unsafeNavigate and Router.unsafeReplace

Two methods similar to Router.navigate and Router.replace but which accept a string as unique argument. Useful for escape hatches.

A quick example with a Redirect component:

const Redirect = ({ to }: { to: string }) => {
  const { url } = Router.useLocation();

  React.useLayoutEffect(() => {
    if (to !== url) {
      Router.unsafeReplace(to);
    }
  }, []);

  return null;
};

// usage
<Redirect to={Router.createURL("root")} />;

encodeSearch and decodeSearch

Encode and decode url search parameters.

import { encodeSearch, decodeSearch } from "react-chicane";

encodeSearch({ invitation: "542022247745", users: ["frank", "chris"] });
// -> "?invitation=542022247745&users=frank&users=chris"

decodeSearch("?invitation=542022247745&users=frank&users=chris");
// -> { invitation: "542022247745", users: ["frank", "chris"] }

👷‍♂️ Roadmap

  • Improve documentation
  • Tests, tests, tests
  • Switch to useSyncExternalStore (React 18+)
  • Write a "focus reset" recipe
  • Find a cool logo
  • Create a website (?)

🙌 Acknowledgements