A fresh react router designed for flexible route transitions
cher-ami router API is inspired by wouter, solidify router and vue router API. This repository started from a copy of willybrauner/react-router.
Because managing route transitions with React is always complicated, this router is designed to allow flexible transitions. It provides Stack component who render previous and current page component when route change.
This router loads history , path-parser and debug as dependencies.
API
Components:
<Router />Wrap Link and stack component<Link />Trig current stack<Stack />Wrap previous and current page
Hooks:
useRouterGet router instance from any componentuseLocationGet current location and set new locationuseRouteGet previous and current route objectuseStackAllow to the parent Stack to handle page transitions and refsuseRouteCounterGet global history route counteruseHistoryGet global router history and handle history changes
Middlewares:
langMiddlewarePatch all routes with:langparams
Services:
LangServiceManage:langparams
$ npm i @cher-ami/router -simport React from "react";
import { Router, Link, Stack } from "@cher-ami/router";
const routesList = [
{
path: "/",
component: HomePage,
},
{
path: "/foo",
component: FooPage,
},
];
function App() {
return (
<Router routes={routesList} base={"/"}>
<nav>
<Link to={"/"} />
<Link to={"/foo"} />
</nav>
<Stack />
</Router>
);
}Page component need to be wrapped by React.forwardRef. The handleRef lets
hold transitions, and ref used by <Stack /> component.
import React from "react";
import { useStack } from "@cher-ami/router";
const FooPage = forwardRef((props, handleRef) => {
const componentName = "FooPage";
const rootRef = useRef(null);
// create custom page transitions (example with GSAP)
const playIn = () => {
return new Promise((resolve) => {
gsap.from(rootRef.current, { autoAlpha: 0, onComplete: resolve });
});
};
const playOut = () => {
return new Promise((resolve) => {
gsap.to(rootRef.current, { autoAlpha: 0, onComplete: resolve });
});
};
// register page transition properties used by Stack component
useStack({ componentName, handleRef, rootRef, playIn, playOut });
return (
<div className={componentName} ref={rootRef}>
{componentName}
</div>
);
});Demo codesandbox: simple usage
cher-ami router use path-parser which
accept path parameters. (check
this documentation).
For example, URL /blog/my-article will match with this route object:
const routesList = [
{
path: "/blog/:id",
component: ArticlePage,
},
];You can access route parameters by page component props or by useRoute() hook.
import React, { useEffect, forwardRef } from "react";
import { useRoute } from "@cher-ami/router";
const ArticlePage = forwardRef((props, handleRef) => {
useEffect(() => {
console.log(props.params); // { id: "my-article" }
}, [props]);
// or from any nested components
const { currentRoute } = useRoute();
useEffect(() => {
console.log(currentRoute.props.params); // { id: "my-article" }
}, [currentRoute]);
// ...
});Demo codesandbox: simple usage
Also, it is possible to match a specific route by a simple dynamic route
parameter for the "not found route" case. In this case, the routes object order
declaration is important. /:rest path route need to be the last of
the routesList array.
const routesList = [
{
path: "/",
component: HomePage,
},
{
path: "/foo",
component: FooPage,
},
// if "/" and "/foo" doesn't match with the current URL, this route will be rendered
{
path: "/:rest",
component: NotFoundPage,
},
];Demo codesandbox: not found route
cher-ami router supports nested routes ๐๐ฝ
- Define children routes in initial routes list with
childrenkey;
const routesList = [
{
path: "/",
component: HomePage,
},
{
path: "/foo",
component: FooPage,
// define children routes here
children: [
{
path: "/people",
component: PeoplePage,
},
{
path: "/yolo",
component: YoloPage,
},
],
},
];- Children were defined within the route that render
FooPagecomponent, so you can then create a new router instance in this component.
Only if it's a nested router, you must not pass routes Router props again.
The previous routes array, passed to the root component, will be used
by Router.
Router props base need to be the same than the path who contains children
routes. In this case, /foo will be the new nested router base. The stack will
then be able to render /foo/people and /foo/yolo.
import React from "react";
import { Router, useStack, Stack } from "@cher-ami/router";
const FooPage = forwardRef((props, handleRef) => {
// ...
return (
<div
className="FooPage"
// ...
>
<Router base={"/foo"}>
<Stack />
</Router>
</div>
);
});Demo codesandbox: nested router
ManageTransitions function allows to define, "when" and "in what conditions",
routes transitions will be exectued.
By default, a "sequential" transitions senario is used by Stack component: the previous page play out performs, then the new page play in.
const sequencialTransition = ({ previousPage, currentPage, unmountPreviousPage }) => {
return new Promise(async (resolve) => {
const $current = currentPage?.$element;
// hide new page
if ($current) $current.style.visibility = "hidden";
// play out and unmount previous page
if (previousPage) {
await previousPage.playOut();
unmountPreviousPage();
}
// wait page isReady promise
await currentPage?.isReadyPromise?.();
// show and play in new page
if (currentPage) {
if ($current) $current.style.visibility = "visible";
await currentPage?.playIn?.();
}
resolve();
});
};It's however possible to create a custom transitions senario function and pass
it to the Stack manageTransitions props. In this example, we would like to
create a "crossed" route senario: the previous page playOut performs at the same
time than the new page playIn.
const App = (props, handleRef) => {
const customSenario = ({ previousPage, currentPage, unmountPreviousPage }) => {
return new Promise(async (resolve) => {
// write a custom "crossed" senario...
if (previousPage) previousPage?.playOut?.();
if (currentPage) await currentPage?.playIn?.();
resolve();
});
};
return (
// ...
<Stack manageTransitions={customSenario} />
);
};Demo codesandbox: custom manage transitions
debug is used on this project. It allows to easily get logs informations on development and production modes.
To use it, add this line in your browser console:
localStorage.debug = "router:*"A use case example is available on this repos.
Install dependencies
$ npm iStart dev server
$ npm run devRouter component creates a new router instance.
<Router routes={} base={} historyMode={} middlewares={}>
{/* can now use <Link /> and <Stack /> component */}
</Router>Props:
- routes
TRoute[]Routes list - base
stringBase URL - default: `"/"`` - historyMode
EHistoryMode(optional) choose history mode. - default :EHistoryMode.BROWSERHistory mode can be BROWSER , HASH , MEMORY . For more information, check the history library documentation - middlewares
[]add routes middleware function to patch each routes (check langMiddleware example)
Trig new route.
<Link to={} className={} />Props:
- to
string | TOpenRouteParamsPath ex:/fooor{name: "FooPage" params: { id: bar }}. "to" props accepts same params than setLocation. - children
ReactNodechildren link DOM element - onClick
()=> void(optional) execute callback on the click event - className
string(optional) Class name added to component root DOM element
Render previous and current page component.
<Stack manageTransitions={} className={} />Props:
- manageTransitions
(T:TManageTransitions) => Promise<void>(optional) This function allows to create the transition scenario. If no props is filled, a sequential transition will be executed. - className
string(optional) className added to component root DOM element
type TManageTransitions = {
previousPage: IRouteStack;
currentPage: IRouteStack;
unmountPreviousPage: () => void;
};
interface IRouteStack {
componentName: string;
playIn: () => Promise<any>;
playOut: () => Promise<any>;
isReady: boolean;
$element: HTMLElement;
isReadyPromise: () => Promise<void>;
}Get current router instance.
const router = useRouter();Returns:
Current router instance.
Allow the router to change location.
const [location, setLocation] = useLocation();
// give URL
setLocation("/bar");
// or an object
setLocation({ name: "FooPage", params: { id: "2" } });Returns:
An array with these properties:
- location
stringGet current pathname location - setLocation
(path:string | TOpenRouteParams) => voidOpen new route
type TOpenRouteParams = {
name: string;
params?: { [x: string]: any };
};Get previous and current route properties (TRoute)
const { currentRoute, previousRoute } = useRoute();Returns:
An object with these properties:
- currentRoute
TRouteCurrent route object - previousRoute
TRoutePrevious route object
type TRoute = {
path: string;
component: React.ComponentType<any>;
props?: { [x: string]: any };
parser?: Path;
children?: TRoute[];
matchUrl?: string;
fullUrl?: string;
};useStack allows to the parent Stack to handle page transitions and refs.
usage:
import React from "react";
import { useStack } from "@cher-ami/router";
const FooPage = forwardRef((props, handleRef) => {
const componentName = "FooPage";
const rootRef = useRef(null);
const playIn = () => new Promise((resolve) => { ... });
const playOut = () => new Promise((resolve) => { ... });
// "handleRef" will get properties via useImperativeHandle
useStack({
componentName,
handleRef,
rootRef,
playIn,
playOut
});
return (
<div className={componentName} ref={rootRef}>
{/* ... */}
</div>
);
});useStack hook can also receive isReady state from the page component. This
state allows for example to wait for fetching data before page playIn function
is executed.
// ...
const [pageIsReady, setPageIsReady] = useState(false);
useEffect(() => {
// simulate data fetching or whatever for 2 seconds
setTimeout(() => {
setPageIsReady(true);
}, 2000);
}, []);
useStack({
componentName,
handleRef,
rootRef,
playIn,
playOut,
// add the state to useStack
// playIn function wait for isReady to change to true
isReady: pageIsReady,
});
// ...How does it work? useStack hook registers isReady state and isReadyPromise
in handleRef.
manageTransitions can now use isReadyPromise in its own thread senario.
const customManageTransitions = ({ previousPage, currentPage, unmountPreviousPage }) => {
return new Promise(async (resolve) => {
// ...
// waiting for page "isReady" state to change to continue...
await currentPage?.isReadyPromise?.();
// ...
resolve();
});
};Demo codesandbox: wait-is-ready
Parameters:
- componentName
stringName of current component - handleRef
MutableRefObject<any>Ref handled by parent component - rootRef
MutableRefObject<any>Ref on root component element - playIn
() => Promise<any>(optional) Play in transition - default:new Promise.resolve() - playOut
() => Promise<any>(optional) Play out transition - default:new Promise.resolve() - isReady
boolean(optional) Is ready state - default:true
Returns:
nothing
Returns route counter
const { routeCounter, isFirstRoute, resetCounter } = useRouteCounter();Parameters:
nothing
Returns:
An object with these properties:
- routerCounter
numberCurrent route number - default:1 - isFirstRoute
booleanCheck if it's first route - default:true - resetCounter
() => voidReset routerCounter & isFirstRoute states
Allow to get the global router history and execute a callback each time history change.
const history = useHistory((e) => {
// do something
});Parameters:
- callback
(event) => voidCallback function to execute each time the history change
Returns:
- history
location[]: Location array of history API
Patch all first level routes with :lang params. For it to work, we need to
initialize LangService first.
import { langMiddleware } from "@cher-ami/router";
<Router routes={routesList} base={"/"} middlewares={[langMiddleware]}>
// ...
</Router>;Manage :lang params from anywhere inside Router scope.
import { LangService, langMiddleware } from "@cher-ami/router";
import { Stack } from "./Stack";
const baseUrl = "/";
// first lang object is default lang
const locales = [{ key: "en" }, { key: "fr" }, { key: "de" }];
// optionally, default lang can be defined explicitly
// const locales = [{ key: "en" }, { key: "fr", default: true }, { key: "de" }];
// initialize LangService
LangService.init(locales, true, baseUrl);
<Router routes={routesList} base={baseUrl} middlewares={[langMiddleware]}>
<App />
</Router>;Inside the App
function App() {
return (
<div>
<button onClick={() => LangService.setLang({ key: "de" })}>
switch to "de" lang
</button>
<nav>
{/* will return /de */}
<Link to={"/"} />
{/* will return /de/foo */}
<Link to={"/foo"} />
</nav>
<Stack />
</div>
);
}Methods:
Initialize LangService. Need to be call before first router instance
languages: list on language objectsshowDefaultLangInUrl: choose if default language is visible in URL or notbase: set the same than router base
LangService.init([{ key: "en" }, { key: "fr" }], true, "/base");Return languages list
const langages = LangService.languages;Return current Language object.
const lang = LangService.currentLang;
// { key: "..." }Return default language object
const defaultLang = LangService.defaultLang;
// { key: "..." }Return LangService init state
const isInit = LangService.isInit;Switch to another available language. This method can be called in nested router component only.
forcePageReload: choose if we reload the full application or using the internal router stack to change the language
LangService.setLang({ key: "de" });If URL is /, showDefaultLangInUrl is set to true and default lang is 'en',
it will redirect to /en. This method can be called in nested router component
only.
forcePageReload: choose if we reload the full application or using the internal router stack to change the language
LangService.redirect();