A simple and safe router for React and TypeScript.
yarn add react-chicane
git clone [email protected]:zoontek/react-chicane.git
cd react-chicane/example
yarn install && yarn dev
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).
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>);
// β¦
};
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"
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)
},
);
type Location = {
url: string;
path: string[];
search: Record<string, string | string[]>;
hash?: string;
};
Router.location; // Location
Navigate to a given route.
Router.navigate("root");
Router.navigate("users");
Router.navigate("user", { userId: "zoontek" });
Same as navigate
, but will replace the current route in the browser history.
Router.replace("root");
Router.replace("users");
Router.replace("user", { userId: "zoontek" });
Go back in browser history.
Router.goBack();
Go forward in browser history.
Router.goForward();
Safely create internal URLs.
Router.createURL("root"); // -> "/"
Router.createURL("users"); // -> "/users"
Router.createURL("user", { userId: "zoontek" }); // -> "/users/zoontek"
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>);
};
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>;
Listen and react on Router.location
changes.
const App = () => {
const location: Location = Router.useLocation();
React.useEffect(() => {
console.log("location changed", location);
}, [location]);
// β¦
};
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?",
);
// β¦
};
Subscribe to location changes. Useful to reset keyboard focus.
const App = () => {
React.useEffect(() => {
const unsubscribe = Router.subscribe((location: Location) => {
resetKeyboardFocusToContent();
});
return unsubscribe;
}, []);
// β¦
};
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")} />;
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"] }
- Improve documentation
- Tests, tests, tests
- Switch to
useSyncExternalStore
(React 18+) - Write a "focus reset" recipe
- Find a cool logo
- Create a website (?)
- react-router for the
history
and theLink
creation code. - reach-router for the path ranking algorithm.