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 providesurpassBlocker
flag insidenavigate.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"),
]);
createRouting
API - navigation inside application. To use outside of react tree. Uses navigateTo
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
- uses useNavigateTo
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");
- React component to make redirect. Navigate
import { Navigate } from "./Router";
<Navigate route="HOME" />;
- get matching routes configs for the current route or previous one. getMatchRoutes
import { getMatchRoutes } from "./Router";
getMatchRoutes({ previousRoute: true });
// to match current route
getMatchRoutes({ pathname: window?.location?.pathname });
or;
getMatchRoutes(); // it is the same
- React base component to make links. Link
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>;
- Same as Link but check if link route is current one and then add NavLink
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>;
- To be used inside getConfig
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
- Get path for requested route, can pass to it search and query params getPath
import { getPath } from "./Route";
const path = getPath("HOME");
- uses getPathGlobal
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
and isRoute
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
- Get root routing caomponent to render inside main react component. It will create Routes for application getRoutingComponent
export const Router = getRoutingComponent(PATHS)((getConfig) => [getConfig("HOME"), getConfig("404")]);
history
API its olny give few more things on top of history package
- give you access to previous route, you can also clear previous route eg after user logs out withPreviousRoute
import { history } from "./Router";
type PreviousRoute = null | Pick<BrowserHistory, "location" | "action">;
const previous: PreviousRoute = history.getPreviousRoute();
history.clearPreviousRoute(); // clear previous route
- with that while navigating with any navigation function/component - pass state flag withPreserveQuery
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
},
});
- create navigaton blocking functionality with more control over withBlocker
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)
queryString
API Create small zustand
store to cache deserialized current query string. also give you few methods to get and update it
- helpers to serialize and deserialize query string. Ir deserialize numbers, undefined, null correctly, not in string form! qs
import { qs } from "./Router";
const queryString = qs.serialize({ obj: { a: 1 } });
const queryObject = qs.deserialize("obj.a=1");
- zustand store, also returns 2 react ooks to get query params useQueryParams
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 }));