@finalapp/react-safe-router

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

Usage no npm install needed!

<script type="module">
  import finalappReactSafeRouter from 'https://cdn.skypack.dev/@finalapp/react-safe-router';
</script>

README

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 queryString parser - you can provide deep nested values as search params, also you get hooks to get current search params

Get started

In index.tsx

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


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

In Router.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 BASE: RouteConfig<BasePaths> = {
  HOME: {
    path: "/",
    Component: PageHome,
  },
  "404": {
    path: "/404",
    Component: Page404,
  },
  "USERS: {
    path: "/users/:id",
    Component: Page404,
    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

import { navigateTo } from "./Router";

navigateTo("HOME");
navigateTo("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.

import { useNavigateTo } from "./Router";

useNavigateTo("HOME");

Navigate - React component to make redirect.

import { Navigate } from "./Router";

<Navigate route="HOME" />;

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

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.

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

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.

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

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

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

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

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

API history

its olny give few more things on top of history package

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

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

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

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

navigateTo("HOME", { state: { preserveQuery: true } }); // dont clear current query on route change
navigateTo("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

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 queryString

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

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

import { qs } from "./Router";

const queryString = qs.serialize({ obj: { a: 1 } });
const queryObject = qs.deserialize("obj.a=1");

useQueryParams - zustand store, also returns 2 react ooks to get query params

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

import { useQueryParamsSelector, useQueryParamsGetByPath, useQueryParams } from "./Router";

useQueryParams.getState(); // returns all state typed above
const param = useQueryParams.getState().getParam("object.param1");
const params = useQueryParams.getState().getParams((params) => params.object?.param1 || null);
useQueryParams.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 = useQueryParamsGetByPath("object.param1");
const params = useQueryParamsSelector(); // all params
const paramsSelected = useQueryParamsSelector((p) => ({ param1: p.object.param1, param2: p.object.param2 }));