Chicane

repository·main·Indexed 19 days ago

https://github.com/zoontek/chicane

A simple and safe router for React and TypeScript applications. Chicane provides typed routes, a Link component, and hooks like useRoute, useLocation, and useBlocker to ensure type-safe URL navigation and a seamless developer experience.

Tokens
14.9K
Snippets
67
Records
76
Agent score
66%

What's inside @zoontek/chicane

  1. Capture query parameters as arrays

    main

    If a query parameter can appear multiple times (e.g., /users?status=Active&status=Inactive), suffix the parameter name with [] in the route pattern. This tells Chicane to treat the parameter as an array. The resulting type will be a nullable string[].

    const Router = createRouter({
      UserList: "/users?:sortBy&:status[]",
    });
    // A match on "/users?status=A&status=B" results in params: { status: ["A", "B"] }
  2. Use path parameters in routes

    main

    You can define dynamic segments in a path using the :paramName syntax. A matched path parameter will result in a non-nullable string in the route's params object.

    const Router = createRouter({
      UserDetail: "/users/:userId",
    });
    // A match on "/users/123" results in params: { userId: "123" }
  3. Use wildcards for subroute delegation

    main

    A wildcard * allows a route to match any path starting with the defined prefix. This is useful for delegating subroute management to a child component.

    Note: Because a wildcard acts as a scope rather than a precise URL, you cannot create a link that points directly to a wildcard route (e.g., you can link to a specific child route, but not the wildcard parent itself).

    const Router = createRouter({
      Home: "/",
      UserArea: "/users/*", // matches "/users" and "/users/:userId"
      UserList: "/users",
      UserDetail: "/users/:userId",
    });
    
    // Pattern for delegating subroutes:
    // 1. Parent component matches the wildcard route (e.g., "UserArea")
    // 2. Child component (UserArea) uses Router.useRoute() with specific sub-routes (e.g., "UserList", "UserDetail")
  4. Define union parameters for restricted types

    main

    To restrict a parameter to specific allowed values (instead of a generic string), use the :name{val1|val2|...} syntax. This allows for precise TypeScript typing in your route handlers.

    const Router = createRouter({
      Projects: "/:env{live|sandbox}/projects",
      Users: "/users?:statuses{invited|enabled|banned}[]",
    });
    
    // Usage in a component:
    // env type is "live" | "sandbox"
    // statuses type is Array<"invited" | "enabled" | "banned"> | undefined
  5. Core design principles of Chicane

    main

    Chicane is built around four key principles to improve the developer experience in React applications:

    • Typed routes: Ensures all route parameters are type-safe, improving DX and preventing runtime errors.
    • Component-friendly: Designed to integrate seamlessly into the React component lifecycle.
    • Easy-to-use: Encourages naming routes rather than manually constructing unsafe URL strings.
    • Performant: Optimized to avoid unnecessary re-renders during navigation.
  6. Use query parameters in routes

    main

    To capture query parameters, use the :paramName syntax following a ? character. Multiple query parameters should be separated by & within the pattern string. Query parameters are nullable in the params object.

    const Router = createRouter({
      UserList: "/users?:sortBy",
    });
    // A match on "/users?sortBy=asc" results in params: { sortBy: "asc" }
    // A match on "/users" results in params: { sortBy: undefined }
  7. Quickstart: Create and use a router

    main

    To use Chicane, follow these steps:

    1. Define your routes: Use createRouter to map route names to path patterns. Path patterns can include dynamic segments like :userId.
    2. Access the current route: Inside a React component, use the useRoute hook provided by your router instance. Pass an array of the route names you want to handle.
    3. Handle routes with pattern matching: The route object returned by useRoute is a discriminated union. You can use a library like ts-pattern to safely match the route name and access strongly typed params for dynamic segments.
    import { createRouter } from "@zoontek/chicane";
    import { match } from "ts-pattern";
    
    const Router = createRouter({
      Home: "/",
      Users: "/users",
      User: "/users/:userId",
    });
    
    const App = () => {
      const route = Router.useRoute(["Home", "Users", "User"]);
    
      // route object is a discriminated union
      return match(route)
        .with({ name: "Home" }, () => <h1>Home</h1>)
        .with({ name: "Users" }, () => <h1>Users</h1>)
        .with({ name: "User" }, ({ params }) => <h1>User {params.userId}</h1>) // params are strongly typed
        .otherwise(() => <h1>404</h1>);
    };
  8. Build the documentation website for production

    main

    To generate static content for the documentation website, run pnpm build. The output will be placed in the build directory, which can then be served by any static hosting service.

    $ pnpm build
  9. Enable Server-side rendering with UrlProvider

    main

    To enable server-side rendering (SSR) in your Chicane application, you must wrap your application component with the UrlProvider imported from @zoontek/chicane/server. This provider allows the server to communicate the current request URL to the application, ensuring that routing and data fetching logic can resolve correctly on the server before the client takes over.

    import { UrlProvider } from "@zoontek/chicane/server";
    import express from "express";
    import { renderToString } from "react-dom/server";
    import { App } from "../client/App";
    
    const app = express();
    
    app.use("*", (req, res) => {
      const html = renderToString(
        <UrlProvider value={req.originalUrl}>
          <App />
        </UrlProvider>,
      );
    
      // …
    });