🚃
cher-ami router
A fresh high-level react router designed for flexible route transitions
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-to-regexp
and @cher-ami/debug as dependencies.
Playground
Summary
API
Components:
Hooks:
useRouter
Get current router informations like currentRoute and previousRouteuseLocation
Get current location and set new locationuseStack
Allow to the parent Stack to handle page transitions and refsuseRouteCounter
Get global history route counteruseHistory
Execute callback each time history changesuseLang
get and set langService current language object
changes
Services:
Global:
Helpers
Global Routers helpersRouters object
Global Routers object contains all routers properties (history, instances...)
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>
)
})
Dynamic routes
cher-ami router use path-to-regexp 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])
})
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,
},
]
Sub-router
cher-ami router supports nested routes from sub routers instance 🙏🏽.
It is possible to nest as many routers as you want.
- Define children routes in initial routes list with
children
property;
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.
-
The new subRouter needs his own base and routes list, getSubRouterBase
and getSubRouterRoutes
functions are available to get them.
import React from "react"
import {
Router,
useStack,
Stack,
useRouter,
getPathByRouteName,
getSubRouterBase,
getSubRouterRoutes,
} from "@cher-ami/router"
const FooPage = forwardRef((props, handleRef) => {
const router = useRouter()
const path = getPathByRouteName(router.routes, "FooPage")
const subBase = getSubRouterBase(path, router.base, true)
const subRoutes = getSubRouterRoutes(path, router.routes)
return (
<div>
<Router base={subBase} routes={subRoutes}>
<Stack />
</Router>
</div>
)
})
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} />
)
}
SSR Support
This router is compatible with SSR due to using staticLocation
props instead of history
props on Router instance.
In this case, the router will match only with staticLocation
props value and render the appropiate route without invoking the browser history. (Because window
is not available on the server).
<Router
routes={routesList}
staticLocation={"/foo"}
>
</Router>
In order to use this router on server side, we need to be able to request API on the server side too.
In this case, request will be print as javascript window object on the renderToString html server response.
The client will got this response.
To be able to request on server side (and on client side too), getStaticProps
route property is available:
{
path: "/article/:slug",
component: ArticlePage,
name: "Article",
getStaticProps: async (props, currentLang) => {
const res = await fetch(`https://api.com/posts/${currentLang.key}/${props.params.slug}`);
const api = await res.json();
return { api };
}
}
Then, get the response data populated in page component props:
function HomePage({ api }) {
return <div>{api.title}</div>
}
For larger example, check the example-ssr folder.
Workflow
# Install dependencies
pnpm i
#
pnpm run build:watch
#
pnpm run test:watch
#
pnpm run dev
#
pnpm run pre-publish
#
npm version {patch|minor|major}
#
npm publish
API
Router
Router component creates a new router instance.
<Router routes={} base={} history={} staticLocation={} middlewares={} id={}>
{}
</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 \ - isHashHistory
boolean
(optional) default false
. If you use HashHistory
, you must set isHashHistory
to true
- staticLocation
string
(optional) use static URL location matching instead of history - middlewares
[]
(optional) add routes middleware function to patch each routes) - id
?number | string
(optional) id of the router instance - default : 1
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 - setPaused
(paused:boolean) => void
Paused router instance - getPaused
() => void
Get paused state of router instance
type TRoute = Partial<{
path: string | { [x: string]: string }
component: React.ComponentType<any>
base: string
name: string
parser: Match
props: TRouteProps
children: TRoute[]
url: string
params?: TParams
queryParams?: TQueryParams
hash?: string
getStaticProps: (props: TRouteProps, currentLang: TLanguage) => Promise<any>
_fullUrl: string
_fullPath: string
_langPath: { [x: string]: string } | null
_context: TRoute
}>
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?: TParams
queryParams?: TQueryParams
hash?: string
}
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
History
: global history object. (Routers.history
)
useLang
Get and update langService current language object.
const [lang, setLang] = useLang()
useEffect(() => {
}, [lang])
setLang("en")
setLang({ key: "en" })
Returns:
Array of :
- lang
TLanguage
: current lang object - setLang
(lang: TLanguage | string, force: boolean) => void
: set new lang object (same API than langService.setLang
)
LangService
Manage :lang
params from anywhere inside Router scope.
Add isHashHistory
to true
if you are using createHashHistory()
for the router.
import { LangService } from "@cher-ami/router"
import { Stack } from "./Stack"
const base = "/"
const languages = [{ key: "en" }, { key: "fr" }, { key: "de" }]
const langService = new LangService({
languages,
showDefaultLangInUrl: true,
base,
})
;<Router
langService={langService}
routes={routesList}
base={base}
//isHashHistory={true} // Optional, only if history is hashHistory
>
<App />
</Router>
Inside the App
function App() {
const { langService } = useRouter()
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:
constructor({ languages: TLanguage[]; showDefaultLangInUrl?: boolean; base?: string; }) void
Initialize LangService by passing it to "langService" Router props
constructor object properties:
languages
: list on language objectsshowDefaultLangInUrl
: choose if default language is visible in URL or notbase
: set the same than router baseisHashHistory
: set to true if hashHistory is used (optional, default false)
const langService = new LangService({
languages: [{ key: "en" }, { key: "fr" }],
showDefaultLangInUrl: true,
base: "/",
})
langService
instance is available in Router scope from useRouter()
hook.
const Page = () => {
const { langService } = useRouter()
}
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
isHashHistory boolean
Return isHashHistory state.
const isHashHistory = langService.isHashHistory
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
.
forcePageReload
: choose if we reload the full application or using the
internal router stack to change the language
langService.redirectToDefaultLang()
Same than redirectToDefaultLang
method but redirect to the user navigator.language
.
If the browser language doesn't exist in Languages array, we redirect to the default lang.
langService.redirectToBrowserLang()
Translate Path
Paths can be translated by lang in route path property. This option works only if LangService instance is created and passed to the Router component.
{
path: { en: "/foo", fr: "/foo-fr", de: "/foo-de" },
component: FooPage,
}
Helpers
createUrl()
(args: string | TOpenRouteParams, base?:string, allRoutes?: TRoute[]) => string
Create a formated URL by string, or TOpenRouteParams
openRoute()
(args: string | TOpenRouteParams, history?) => void
Push new route in current history. Stack(s) component(s) will return the appriopriate route.
Routers
Routers is a global object who contains all routers informations. Because @cher-ami/router is possibly multi-stack, we need a global object to store shared informations between router instances.
Routers.routes
TRoute[]
Final routes array used by the router be
Routers.history
HashHistory | MemoryHistory | BrowserHistory
Selected history mode. all history API is avaible from this one.
Routers.isHashHistory
boolean
Return the value set on the Router component
Routers.langService
LangService
LangService instance given to the first Router component.
Routers.routeCounter
number
How many route are resolved from the start of the session. This property is also available from useRouteCounter
.
Routers.isFirstRoute
boolean
Is it the first route of the session. This property is also available from useRouteCounter
.
Thanks
cher-ami router API is inspired by wouter,
solidify router
and
vue router API.
Credits
Willy Brauner
& cher-ami