Giter Club home page Giter Club logo

router's Introduction

🚃
cher-ami router

A fresh high-level react router designed for flexible route transitions

npm build


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 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 Execute callback each time history changes
  • useLang get and set langService current language object changes

Services:

Global:

  • Helpers Global Routers helpers
  • Routers 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)

  // create custom page transitions (example-client 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>
  )
})

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) // { id: "my-article" }
  }, [props])

  // or from any nested components
  const { currentRoute } = useRouter()
  useEffect(() => {
    console.log(currentRoute.props.params) // { id: "my-article" }
  }, [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,
  },
  // if "/" and "/foo" doesn't match with the current URL, this route will be rendered
  {
    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.

  1. Define children routes in initial routes list with children property;
const routesList = [
  {
    path: "/",
    component: HomePage,
  },
  {
    path: "/foo",
    component: FooPage,

    // define children routes here
    children: [
      {
        path: "/people",
        component: PeoplePage,
      },
      {
        path: "/yolo",
        component: YoloPage,
      },
    ],
  },
]
  1. Children were defined within the route that render FooPage component, so you can then create a new router instance in this component.

  2. 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()
  // Parsed routes list and get path by route name -> "/foo"
  const path = getPathByRouteName(router.routes, "FooPage")
  // (if last param is false, '/:lang' will be not added) -> "/base/:lang/foo"
  const subBase = getSubRouterBase(path, router.base, true)
  // get subRoutes
  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

    // 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()
  })
}

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) => {
      // write a custom "crossed" senario...
      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"}
  // history={createBrowserHistory()}
>
  // ...
</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) => {
      // props contains route props and params (ex: slug: "article-1")
      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

## build watch
pnpm run build:watch

## start tests
pnpm run test:watch

## start all examples
pnpm run dev

## Before publishing
pnpm run pre-publish

## Increment version
npm version {patch|minor|major}

## Publish
npm publish

API

Router

Router component creates a new router instance.

<Router routes={} base={} history={} staticLocation={} middlewares={} id={}>
  {/* can now use <Link /> and <Stack /> component */}
</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
  • 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
// previousRoute and currentRoute
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 // full URL who not depends on current instance
  _fullPath: string // full Path /base/:lang/foo/second-foo
  _langPath: { [x: string]: string } | null
  _context: TRoute
}>

useLocation

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 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) => {  ... });

  // "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 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) => {
  // do something
})

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(() => {
  // when current lang change
  // it's usefull only if setLang method do not refresh the page.
}, [lang])

// set new lang with lang object "key" property value only
setLang("en")
// set new lang with the lang object
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.

import { LangService } from "@cher-ami/router"
import { Stack } from "./Stack"

const base = "/"

// first lang object is default lang
const languages = [{ key: "en" }, { key: "fr" }, { key: "de" }]
// optionally, default lang can be defined explicitly
// const languages = [{ key: "en" }, { key: "fr", default: true }, { key: "de" }];

// Create LangService instance
const langService = new LangService({
  languages,
  showDefaultLangInUrl: true,
  base,
})

;<Router langService={langService} routes={routesList} base={base}>
  <App />
</Router>

Inside the App

function App() {
  // get langService instance from router context
  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 objects
  • showDefaultLangInUrl: choose if default language is visible in URL or not
  • base: set the same than router base
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()
  // langService.setLang() ...
}

languages Tlanguage[]

Return languages list

const langages = langService.languages

currentLang TLanguage

Return current Language object.

const lang = langService.currentLang
// { key: "..." }

defaultLang TLanguage

Return default language object

const defaultLang = langService.defaultLang
// { key: "..." }

isInit boolean

Return langService init state

const isInit = langService.isInit

setLang(toLang: TLanguage, forcePageReload = true) void

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" })

redirectToDefaultLang(forcePageReload = true) void

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()

redirectToBrowserLang(forcePageReload = true) void

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.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

router's People

Contributors

hlefrant avatar pierregradelet avatar willybrauner avatar yoanngueny avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Forkers

yoanngueny

router's Issues

Fix log

  • replace "Use" by "You

router:helpers route.path object is not supported without langService. Use should use route.path string instead.

  • Check if the log is important.

Create Language middleware

  • - patcher toutes routes avec le param :lang
  • - gérer la langue par défaut
  • - rediriger vers la langue par défaut si pas de langue définie (ex: si on est /contact -> /fr/contact)
  • - gérer l'option de l'affichage de la langue par default dans l'URL
  • - détection de la langue par navigateur (optionnel / boolean) + mémoriser la langue choisie (localStorage)
  • - méthode pour changer de langue

uselayouteffect-ssr for common fixes.

uselayouteffect-ssr for common fixes.
Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes.

Page not found only displayed at one level

Example :

{
    path: "/:rest",
    component: PageNotFound,
    name: EPage.NOT_FOUND,
  },

This route will only be triggered at the first level /bad_route.
But it will not be triggered at the second and others /bad_route/another_bad_route.

Is it possible to create stack navigation?

Hi i just found the lib, congrats on v2 launch! looks like a really good lib.
Seems the page transition is first class support here.

Is it possible to create stack based navigation? The goal is to have webapp but behave like native app.
It retains the previous state.

Let's say i have page A and B, and inside page A i have an input using useState.
If i go to page B, then press go back(-1) then A will lose it's state since it's unmounted.

Is it possible to have greater control over unmount?
So, instead of unmount it, just hide it, so previous page can retain it's state.

If we are using example above then it would be like:

When on page A, navigate go to page B.
then inside the Stack div element, we still have 2 div, which is A and B. but the A div is hidden.

After we arrive on page B, if we click back -1, then the Stack div will only have A div.
Is it possible?

Thanks

Fix langService import

Error append if LangService object export is imported instead of default singleton from index.

Enable Link "to" object props

Need to set object to 'to' Link props like setLocation.

<Link to={ { name:"PageName", params: {id:foo} } }>...</Link>

Translate path

  {
    path: { en: "/notebook", fr: "/carnet" },
    component: NoteBookPage,
    name: EPage.NOTEBOOK_PAGE,
  },

Can't use with react 17?

I hope to use the router with react 17, some of the components i will be using still cannot be used with react 18.
Since the beta version is compatible with react 17, is there any problem if package.json is using "react": "^17.0.2 || ^18.0.0"?

Thank you

Can't create nested route with the same first route level name

I would like to call one page for /discover and another page for /discover/detail, but it creates an error.

routes.ts :

{
    path: "/discover",
    component: PageDiscoverHome,
  },
  {
    path: "/discover",
    component: PageDiscoverList,
    children: [
      {
        path: "/detail",
        component: PageDiscoverDetail,
      },
    ],
  },

Error when nested route are called :

Router id 2 > no routes array is set.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.