npm package: @finalapp/react-safe-router
Description: **Routing extensions and type safety for `react-router` and `history` packages**
Author: finalapp
Latest version: 0.6.1

# React safe router

**Routing extensions and type safety for `react-router` and `history` packages**

---

## Installation

`yarn add @finalapp/react-safe-router`

or

## `npm i @finalapp/react-safe-router`

## History

We are extending history package with useful helpers:

- `withBlocker` - to gain more control over blocker functionality - we can block routing completely anywhere inside our
  code (not only inside react components tree). Also provide `surpassBlocker` flag inside `navigate.state` object to
  navigate anyway even if we have active blocker.
- `withPreserveQuery` - flag to indicate if next route shouldn't clear query params. Instead of flag we can pass a
  function which gain acces to query and can return any query string.
- `withPreviousRoute` - to know on which route user was before the current one.

---

## Routing

- Type safety! - if you initialize routing with `createRouting` helper - you get overriden navigate, Link, Navigate
  functions from react-router wich are type safe and will help you with navigation, refactorings etc.
- `isRoute | useIsRoute` - type safe function to check if current route matches to route name which you will provide as an argument
- middleware field inside routing config object - run middleware functions which can check anything and return differet component or navigate component to redirect the user
- rest functions are just overrides from `react-router` with changed arguments to be route name which you will provide
  with configuration
- custom searchString parser - you can provide deep nested values as search params, also you get hooks to get current search params

## Get started

_In index.tsx_

```tsx
...
import { HistoryRouter } from "@finalapp/react-safe-router";
import { Router } from "./Router";
...


render(
  <HistoryRouter>
    <Router />
  </HistoryRouter>
  document.getElementById("root"),
);
```

_In Router.tsx_

```tsx
...
import { RouteConfig, RouteValue, createRouting } from "@finalapp/react-safe-router";
...

type BasePaths = {
  HOME: RouteValue;
  "404": RouteValue;
  USERS: RouteValue<{ params: { id: string }, search: { isFriend: boolean }, state?: { isABTest: boolean } }>;
};

const isLogged = () => true;
const hasPermission = (permission: "USER" | "ADMIN") => () => true;

const BASE: RouteConfig<BasePaths> = {
  HOME: {
    path: "/",
    Component: PageHome,
  },
  "404": {
    path: "/404",
    Component: Page404,
  },
  "USERS: {
    path: "/users/:id",
    Component: PageUsers,
    middleware: [isLogged, hasPermission("USER")]
  }
};

export type Paths = BasePaths;

export const {
  navigateTo,
  Navigate,
  getMatchRoutes,
  Link,
  NavLink,
  getPath,
  getPathGlobal,
  getRoutingComponent,
  isRoute,
  useIsRoute,
  useNavigateTo,
} = createRouting<Paths>();

export const Router = getRoutingComponent(BASE)((getConfig) => [
  getConfig("HOME"),
  getConfig("USERS"),
  getConfig("404"),
]);
```

# API `createRouting`

##

`navigateTo` - navigation inside application. To use outside of react tree. Uses `history` functions under the hood

```tsx
import { navigateTo } from "./Router";

navigateTo({ route: "HOME" });
navigateTo({ route: "USERS", params: { id: "1" }, search: { isFriend: false } }); // typescript will complain if we wont provide needed arguments provided in `BasePaths` type
```

##

`useNavigateTo` - uses `navigateTo` but instead of direct `history` functions - uses `react-router` - `useNavigate`. Its convinient to use it because it hase more checks to avoid navigate to same routes etc.

```tsx
import { useNavigateTo } from "./Router";

useNavigateTo({ route: "HOME" });
```

##

`Navigate` - React component to make redirect.

```tsx
import { Navigate } from "./Router";

<Navigate route="HOME" />;
```

##

`getMatchRoutes` - get matching routes configs for the current route or previous one.

```tsx
import { getMatchRoutes } from "./Router";

getMatchRoutes({ previousRoute: true });

// to match current route
getMatchRoutes({ pathname: window?.location?.pathname });
or;
getMatchRoutes(); // it is the same
```

##

`Link` - React base component to make links.

```tsx
import { Link } from "./Route";

<Link route="HOME">Go to home page</Link>;
<Link route="USERS" state={{ isABTest: true }} params={{ id: "2" }} search={{ isFriend: false }}>
  User info
</Link>;
```

##

`NavLink` - Same as Link but check if link route is current one and then add `active` className if it is

```tsx
import { NavLink } from "./Route";

// if current route is HOME - then link element has `actvive` className
<NavLink route="HOME">Go to home page</Link>;
```

##

`getConfig` - To be used inside `getRoutingComponent`, provide route name and it will parse it to `RouteObject` used isnide `react-router`. If your route is nested - just nest it with `getConfig` also. Nesting is second argument - array of `getConfig` calls.

```tsx
import { getConfig } from "./Route";

getConfig("HOME");
getConfig("HOME", [getConfig("HOME_NESTED")]); // with nested routes
```

##

`getPath` - Get path for requested route, can pass to it search and query params

```tsx
import { getPath } from "./Route";

const path = getPath("HOME");
```

##

`getPathGlobal` - uses `getPath` but return path with location part, second argument can be omitted then default will be `window.location.origin`

```tsx
import { getPathGlobal } from "./Route";

const path = getPathGlobal("HOME", "http://site.com"); // custom origin
const path = getPathGlobal("HOME"); // witout second parameter - default `widnow.location.origin` will be used
```

##

`isRoute` and `useIsRoute` - check if provided route name is current or previous one

```tsx
import { isRoute } from "./Route";

// useIsRoute has same arguments as isRoute
// one difference is it should be used inside components when you want to sync checking with navigation change
isRoute("HOME"); // check if current route is `HOME`
isRoute("HOME", true); // with flag as second argument - will check if previous route was `HOME`
isRoute(["HOME", "USERS"]); // check for multiple routes is any of them is current one
```

##

`getRoutingComponent` - Get root routing caomponent to render inside main react component. It will create Routes for application

```tsx
export const Router = getRoutingComponent(PATHS)((getConfig) => [getConfig("HOME"), getConfig("404")]);
```

# API `history`

its olny give few more things on top of [history](https://github.com/remix-run/history) package

##

`withPreviousRoute` - give you access to previous route, you can also clear previous route eg after user logs out

```ts
import { history } from "./Router";

type PreviousRoute = null | Pick<BrowserHistory, "location" | "action">;

const previous: PreviousRoute = history.getPreviousRoute();
history.clearPreviousRoute(); // clear previous route
```

##

`withPreserveQuery` - with that while navigating with any navigation function/component - pass state flag `preserveQuery` to dont clear query on route change or return some custom query. Its convinient to use eg after user logs in with some query string and we want to preserve it one log redirect

```ts
import { history, useNavigateTo, qs } from "./Router";

type PreserveQuery = true | ((currentSearch: string) => string);
type PreserveQueryState = { preserveQuery?: PreserveQuery };

navigateTo({ route: "HOME", state: { preserveQuery: true } }); // dont clear current query on route change
navigateTo({
  route: "HOME",
  state: {
    preserveQuery: (query) => qs.serialize({ custom: { nested: true } }), // function to return some different query
  },
});
```

##

`withBlocker` - create navigaton blocking functionality with more control over `react-router` - `useBlocker`

```ts
import { history, useNavigatonBlocker } from "./Router";

type HistoryBlockerState = { surpassBlocker?: boolean };
type BlockerArg = string | (() => boolean);

const unblock = history.blockNavigation(arg: BlockerArg); // block navigation, provide custom blocker function wich can test for some info after user tries to change route, or just a string which will use native window.confirm and you will must inBlock it bu yourself

// also can be used as react hook, `when` arg change can unblock routing automatically
const unblock = useNavigatonBlocker(arg: BlockerArg, when: boolean)
```

# API `searchString`

Create small `zustand` store to cache deserialized current search string. also give you few methods to get and update it

##

`qs` - helpers to serialize and deserialize search string. Ir deserialize numbers, undefined, null correctly, not in string form!

```ts
import { qs } from "./Router";

const searchString = qs.serialize({ obj: { a: 1 } });
const searchObject = qs.deserialize("obj.a=1");
```

##

`useSearchParams` - zustand store, also returns 2 react ooks to get search params

```ts
type SearchParamsState = {
  // current search string deserialized
  search: Record<string, any>;
  // current search string
  searchString: string;
  // get one particular params from search string by path
  getParam: (path: string, state?: SearchParamsState) => any;
  // get all search params or select needed by selector function
  getParams: (selector?: <T = any>(search: SearchParamsState["search"]) => T, state?: SearchParamsState) => any;
  //  updating search string
  updateSearch: (
    params: SearchParamsState["search"],
    merging?: "replace" | "merge" | "deepMerge",
    routeOption?: "replace" | "push",
  ) => void;
};

import { useSearchParamsSelector, useSearchParamsGetByPath, useSearchParams } from "./Router";

useSearchParams.getState(); // returns all state typed above
const param = useSearchParams.getState().getParam("object.param1");
const params = useSearchParams.getState().getParams((params) => params.object?.param1 || null);
useSearchParams.getState().updateSearch(
  { object: { param2: "new" } },
  "deepMerge", // as you can see in above type - you have 3 posibilities, replace qith new one, shallow merge or deep merge
  "replace", // default is `push`, decide what of a route change you want, replace current one or push to new
);

// hooks
const param1 = useSearchParamsGetByPath("object.param1");
const params = useSearchParamsSelector(); // all params
const paramsSelected = useSearchParamsSelector((p) => ({ param1: p.object.param1, param2: p.object.param2 }));
```