🚃
cher-ami router
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.
Why another 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.
Summary
API
Components:
Hooks:
useRouter Get current router informations like currentRoute and previousRoute
useLocation Get current location and set new location
useStack Allow to the parent Stack to handle page transitions and refs
useRouteCounter Get global history route counter
useHistory Get global router history and handle history
changes
Middlewares:
Services:
Installation
$ npm i @cher-ami/router -s
Simple usage
import React from "react";
import { Router, Link, Stack } from "@cher-ami/router";
import { createBrowserHistory } from "history";
const routesList = [
{
path: "/",
component: HomePage,
},
{
path: "/foo",
component: FooPage,
},
];
const history = createBrowserHistory();
function App() {
return (
<Router routes={routesList} history={history} 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);
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 });
});
};
useStack({ componentName, handleRef, rootRef, playIn, playOut });
return (
<div className={componentName} ref={rootRef}>
{componentName}
</div>
);
});
Demo codesandbox: simple usage
Dynamic routes
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 useRouter() hook.
import React, { useEffect, forwardRef } from "react";
import { useRoute } from "@cher-ami/router";
const ArticlePage = forwardRef((props, handleRef) => {
useEffect(() => {
console.log(props.params);
}, [props]);
const { currentRoute } = useRouter();
useEffect(() => {
console.log(currentRoute.props.params);
}, [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,
},
{
path: "/:rest",
component: NotFoundPage,
},
];
Demo codesandbox: not found route
Nested Routes
cher-ami router supports nested routes 🙏🏽
- Define children routes in initial routes list with
children key;
const routesList = [
{
path: "/",
component: HomePage,
},
{
path: "/foo",
component: FooPage,
children: [
{
path: "/people",
component: PeoplePage,
},
{
path: "/yolo",
component: YoloPage,
},
],
},
];
- Children were defined within the route that render
FooPage component, 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
Manage transitions
ManageTransitions function allows to define, "when" and "in what conditions",
routes transitions will be exectued.
Default sequential transitions
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;
if ($current) $current.style.visibility = "hidden";
if (previousPage) {
await previousPage.playOut();
unmountPreviousPage();
}
await currentPage?.isReadyPromise?.();
if (currentPage) {
if ($current) $current.style.visibility = "visible";
await currentPage?.playIn?.();
}
resolve();
});
};
Custom transitions
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) => {
if (previousPage) previousPage?.playOut?.();
if (currentPage) await currentPage?.playIn?.();
resolve();
});
};
return (
<Stack manageTransitions={customSenario} />
);
};
Demo codesandbox: custom manage transitions
Debug
@wbe/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:*"
Example
A use case example is available on this repos.
Install dependencies
$ npm i
Start dev server
$ npm run dev
API
Router
Router component creates a new router instance.
<Router routes={} base={} history={} middlewares={}>
{}
</Router>
Props:
- routes
TRoute[] Routes list
- base
string Base URL - default: "/"
- history
BrowserHistory | HashHistory | MemoryHistory (optional) create and set an history () -
default : BrowserHistory
History 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)
Link
Trig new route.
<Link to={} className={} />
Props:
- to
string | TOpenRouteParams Path ex: /foo or {name: "FooPage" params: { id: bar }}.
"to" props accepts same params than setLocation.
- children
ReactNode children link DOM element
- onClick
()=> void (optional) execute callback on the click event
- className
string (optional) Class name added to component root DOM element
Stack
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>;
}
useRouter
Get current router informations:
const router = useRouter();
Returns:
useRouter() returns an object with these public properties:
- currentRoute
TRoute Current route object
- previousRoute
TRoute Previous route object
- routeIndex
number Current router index
- base
string Formated base URL
type TRoute = {
path: string;
component: React.ComponentType<any>;
props?: { [x: string]: any };
parser?: Path;
children?: TRoute[];
matchUrl?: string;
fullUrl?: string;
};
useLocation
Allow the router to change location.
const [location, setLocation] = useLocation();
setLocation("/bar");
setLocation({ name: "FooPage", params: { id: "2" } });
Returns:
An array with these properties:
- location
string Get current pathname location
- setLocation
(path:string | TOpenRouteParams) => void Open new route
type TOpenRouteParams = {
name: string;
params?: { [x: string]: any };
};
useStack
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) => { ... });
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(() => {
setTimeout(() => {
setPageIsReady(true);
}, 2000);
}, []);
useStack({
componentName,
handleRef,
rootRef,
playIn,
playOut,
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) => {
await currentPage?.isReadyPromise?.();
resolve();
});
};
Demo codesandbox: wait-is-ready
Parameters:
- componentName
string Name 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
useRouteCounter
Returns route counter
const { routeCounter, isFirstRoute, resetCounter } = useRouteCounter();
Parameters:
nothing
Returns:
An object with these properties:
- routerCounter
number Current route number - default: 1
- isFirstRoute
boolean Check if it's first route - default: true
- resetCounter
() => void Reset routerCounter & isFirstRoute states
useHistory
Allow to get the global router history and execute a callback each time history
change.
const history = useHistory((e) => {
});
Parameters:
- callback
(event) => void Callback function to execute each time the
history change
Returns:
- history
location[] : Location array of history API
langMiddleware
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>;
LangService
Manage :lang params from anywhere inside Router scope.
import { LangService, langMiddleware } from "@cher-ami/router";
import { Stack } from "./Stack";
const baseUrl = "/";
const locales = [{ key: "en" }, { key: "fr" }, { key: "de" }];
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:
init(languages: TLanguage[], showDefaultLangInUrl = true, base = "/") void
Initialize LangService. Need to be call before first router instance
languages: list on language objects
showDefaultLangInUrl: choose if default language is visible in URL or not
base: set the same than router base
LangService.init([{ key: "en" }, { key: "fr" }], true, "/base");
languages Tlanguage[]
Return languages list
const langages = LangService.languages;
currentLang TLanguage
Return current Language object.
const lang = LangService.currentLang;
defaultLang TLanguage
Return default language object
const defaultLang = LangService.defaultLang;
isInit boolean
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();
Translate Path
Paths can be translated by lang in route path property:
{
path: { en: "/foo", fr: "/foo-fr", de: "/foo-de" },
component: FooPage,
}
Credits
Willy Brauner
& cher-ami