npm package: @shardbook/react-router
Description: Simple web router for react that supports nested routes, data loaders and inline imports for code-splitting.
Author: Shardbook
Latest version: 1.4.0

# react-router &middot; [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/shardbook/react-components/blob/main/router/LICENSE) [![Published on NPM](https://img.shields.io/npm/v/@shardbook/react-router.svg)](https://www.npmjs.com/package/@shardbook/react-router) [![Minified size](https://img.shields.io/bundlephobia/min/@shardbook/react-router.svg)](https://bundlephobia.com/package/@shardbook/react-router) [![Tree-shakeable](https://badgen.net/bundlephobia/tree-shaking/@shardbook/react-router)](https://github.com/shardbook/react-components/tree/main/router)

Simple web router for react that supports nested routes, data loaders and inline imports for code-splitting.

## Examples

### Using React.Supense and React.lazy

```tsx
import { ComponentType, ComponentProps, lazy, Suspense } from "react";
import { RouterProvider, Outlet, Route } from "@shardbook/react-router";
import { Spinner } from "./Spinner";

// lazy loader function
function lazyLoad<T extends ComponentType<any>>(factory: () => Promise<{ default: T; }>) {
    const Component = lazy(factory);
    return (props: ComponentProps<T>) => (
        <Suspense fallback={<Spinner />}>
            <Component {...props} />
        </Suspense>
    );
}

const SignIn = lazyLoad(() => import("./SignIn"));
const SignUp = lazyLoad(() => import("./SignUp"));
const PageNotFound = lazyLoad(() => import("./PageNotFound"));
const PageError = lazyLoad(() => import("./PageError"));

const routes: Route[] = [
    {
        path: "/",
        element: <SignIn />,
    },
    {
        path: "/sign-up",
        element: <SignUp />,
    }
];

export default function App() {
    return (
        <RouterProvider
            routes={routes}
            noMatch={<PageNotFound />}
            error={(error) => <PageError error={error} />}
        >
            <Outlet setLocation fallback={<Spinner />} />
        </RouterProvider>
    );
}
```

It is always a good idea to wrap a lazy component inside a Suspense to let the user know that we're still importing it.

### Using inline import

```tsx
import { RouterProvider, Outlet, Route } from "@shardbook/react-router";
import { Spinner } from "./Spinner";

const PageNotFound = lazyLoad(() => import("./PageNotFound"));
const PageError = lazyLoad(() => import("./PageError"));

const routes: Route[] = [
    {
        path: "/",
        element: () => import("./SignIn"),
    },
    {
        path: "/sign-up",
        element: () => import("./SignUp"),
    }
];

export default function App() {
    return (
        <RouterProvider
            routes={routes}
            noMatch={<PageNotFound />}
            error={(error) => <PageError error={error} />}
        >
            <Outlet setLocation fallback={<Spinner />} />
        </RouterProvider>
    );
}
```

The router will import the component first before changing the location.
You can use `useRouter().loading` to handle loading state.

### With loader

```tsx
// Dashboard.tsx
export default function Dashboard() {
    return <>Hello world!</>;
}

// User.tsx
export loader(config: any, params: any) {
    return fetch("/api/user" + params.userId, {
        headers: {
            "Authorization": "Bearer " + config.token
        }
    });
}

export default function User(props : { config: any; params: any; data: any; }) {
    return (
        <>
           config: {JSON.stringify(props.config)}
           userId: {props.params.userId}
           data: {JSON.stringify(props.data)}
        </>
    );
}

// Layout.tsx
import { Outlet, Link } from "@shardbook/react-router";
import { Spinner } from "./Spinner";

export default function Layout() {
    return (
        <>
            <header>
                <Link to="/">Dashboard</Link>
                <Link to="/my-user-id">User</Link>
            </header>

            <main>
                <Outlet setLocation fallback={<Spinner />} />
            </main>
        </>
    );
}

// App.tsx
import { RouterProvider, Route } from "@shardbook/react-router";

const PageNotFound = lazyLoad(() => import("./PageNotFound"));
const PageError = lazyLoad(() => import("./PageError"));

const routes: Route[] = [
    {
        path: "/",
        element: () => import("./Dashboard"),
    },
    {
        path: "/:userId",
        element: () => import("./User"),
    }
];

export default function App() {
    return (
        <RouterProvider
            routes={routes}
            noMatch={<PageNotFound />}
            error={(error) => <PageError error={error} />}
            config={{ token: "server-token" }}
        >
            <Layout />
        </RouterProvider>
    );
}
```

If your going to navigate to `/my-user-id`, the router will import the component with the loader first, then call the loader, waits for its response before changing the location. You can use `useRouter().loading` to handle loading state.

## Components

* RouterProvider - Routes controller.
* Outlet - Route element renderer.
* Link - A history-aware `<a>`.

## Hooks

* useRouter - Returns the current location and the loading path.
* useNavigate - Returns an imperative function for route navigation.

## Type declarations

```ts
type RouterProps = {
    /**
     * @required
     * An array of `Route` objects with nested routes on the `children`;
     */
    routes: Route[];
    /**
     * List of route path that the user can access;
     */
    access?: string[];
    /**
     * Configurations that will be pass down to routes and loaders.
     * 
     * Ex. Current user or user token.
     */
    config?: any,
    /**
     * This will be shown if there's no matching route of the current location.
     * @defaultValue "Not found"
     */
    noMatch?: React.ReactNode;
    /**
     * This will be shown if there's an error while loading route.
     */
    error?: (error: string) => React.ReactNode;
    /**
     * @required
     */
    children: React.ReactNode;
};
```

```ts
type Route = {
    /**
     * @required
     * Must have a trailing `/`.
     */
    path: string;
    /**
     * Route element.
     * 
     * Note: This will be ignored if this is a parent route.
     */
    element?: (() => Promise<{
        default: (props?: any) => JSX.Element;
        loader?: (config?: any, params?: any) => Promise<Response>;
    }>) | React.ReactNode;
    /**
     * Route children.
     */
    children?: Route[];
};
```

```ts
type LinkProps = {
    /**
     * Set to `true` to skip client side routing.
     */
    reloadDocument?: boolean;
    /**
     * Set to `true` to use `history.replace`.
     */
    replace?: boolean;
    /**
     * History state.
     */
    state?: any;
    /**
     * Pathname.
     */
    to: string;
} & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href">;
```