Skip to content

Config-based router with support of effector as first-class citizen

License

Notifications You must be signed in to change notification settings

Tauka/effing-router

Repository files navigation

Effing-router

Effing-router is a config-based router with support of effector as first-class citizen

  • Excellent integration with effector
  • Full Typescript support
  • ES modules and tree-shaking support
  • Modularity and customizations

How it works (or why another routing library)

effing-router utilizes different routing model than react-router.

Instead of parsing url and rendering routes that match path, effing-router offers routing by specifying routes (parts of your application) that you want to render, and url gets compiled out of routes as side-effect.

Installation

npm install effing-router effector effector-react --save

# yarn
yarn add effing-router effector effector-react

Getting started

import { router, initializeRouter } from 'effing-router';
import { bindDom } from 'effing-router/dom';
import { RouterView } from 'effing-router/react';

const Main = ({ childRoute }) => {
  return <div>
    <h1> Main </h1>
    <div>
      { childRoute() }
    </div>
  </div>
}

const Profile = () => {
  return <div>
    This is my profile
    <a onClick={() => router.go('news')}> Go to news </a>
  </div>
}

const News = () => {
  return <div>
    News
    <a onClick={() => router.go('profile')}> Go to profile </a>
  </div>
}

const Auth = () => {
  return <div> Signin </div>
}

const routes = [
  {
    name: 'main',
    component: Main,
    children: [
      {
        name: 'profile',
        component: Profile,
        path: '/myProfile'
      },
      {
        name: 'news',
        component: News,
        path: '/news'
      }
    ]
  },
  {
    name: 'auth',
    component: Auth
  }
]

initializeRouter(routes)
bindDom(router, routes)

// rendering routes
export const App = () => {
  return <RouterView
    router={router}
    routesList={routesList}
  />
}

Working with effector

Fetching data on route mount

import { forward, createEffect, restore } from 'effector';
import { router } from 'effing-router';

const fxGetStats = createEffect().use(statsApi);
const $stats = restore(fxGetStats, null);

forward({
  from: router.createMountEvent("dashboard"),
  to: fxGetStats
});

Demo: https://codesandbox.io/s/clever-firefly-89zp4?file=/src/features/Dashboard.tsx

Cleaning up on route unmount

import { createEvent, restore } from 'effector';
import { router } from 'effing-router';

const setData = createEvent<string[]>();
const $data = restore(setData, []);

const evDashboardUnmount = router.createUnmountEvent("dashboard");
$data.reset(evDashboardUnmount);

Demo: https://codesandbox.io/s/great-cache-jfnq9?file=/src/features/Dashboard.tsx

Working with params

import { router } from 'effing-router';

const $userId = router.createParamStore('userId'),
$userId.watch(id => console.log('new user id: ', id))

router.go({ userId: 5 }); // 'new user id: 5'
router.go({ userId: 6 }); // 'new user id: 6'

Demo: https://codesandbox.io/s/determined-jennings-t6fjq?file=/src/features/Dashboard.tsx

Syncing store with url

import { restore, createEvent } from 'effector';
import { router } from 'effing-router';

const setPosition = createEvent();
const positionString = new URLSearchParams(location.search).get('position');
const $position = restore(setPosition, positionString ? JSON.parse(positionString) : { x: 0, y: 0 });

// router.go, router.replace are usual effector events
forward({
  from: $position,
  to: router.replace.prepend((position) => ({ position: JSON.stringify(position) }))
})

Demo: https://codesandbox.io/s/hungry-pare-lrsvx?file=/src/features/Dashboard.tsx

Core API

router

Main instance of router, it is a collection of events and functions that perform routing. Router state consists of two main part: routes and params

{
  routes: ['main', 'users'],
  params: { userId: 5 }
}

Routes specify what you want to render, i.e. what parts of your application, which are identified by their respective name on routes list.

Params specifty how you want to render, it can contain any data related to route state. You can persist any application data in params.

initializeRouter(router, routesList)

Accepts router object and list of routes configuration

Route configuration shape

{
  name: 'main',
  component: Main,
  children: [
    {
      name: 'news',
      component: News,
      path: '/news/:newsId'
    },
    {
      name: 'users',
      component: Users,
      redirect: {
        condition: $noUsers,
        to: 'news'
      }
    }
  ]
}

Only required prop is name. However, if you use RouterView from effing-router/react, you will have to specify component

Redirects

Redirect configuration can be specified for each route, it consists of two required properties: condition and to.

condition is a boolean store, and if has value true, redirects to to. It is reactive, which means redirect can be triggered both on router.go and when condition store becomes true

to can be any form of argument of router.go, basically router.go gets called with to

Compiling path

path is used to compile url. When specifiying route, the grandest child's path will be used, parents path will be ignored

// state
{
  routes: ['main', 'news'],
  params: { newsId: 7 }
}

/*
* router will look for 'news' route's path,
* because it is present, path will be compiled to
* 
* /news/7
*/

If not specified, path will be compiled from route names concatenated with / as delimeter, all params will be compiled with URLSearchParams.

// state
{
  routes: ['main', 'users'],
  params: { userId: 5 }
}

/*
* there is not path specified for 'users',
* by default it will be compiled to
*
* /main/users?userId=5
*/

router.go

Used to navigate to new route

import { router } from 'effing-router';

// relative routing, changes between siblings
// ['main', 'news'] => ['main', 'music']
router.go('music');

// absolute routing, does not change params
router.go(['main', 'music']);

// routing based on previous route
router.go(prevRoute => {
  routes: [...prevRouter.routes, 'main', 'music'],
  params: {
    ...prevRouter.params
    userCount: prevRouter.params.userCount + 1
  }
});

// writes new key-value params, overwrites if exists
router.go({ userId: 5 });

You can find more overloads in docs.

router.replace

It has same signatures as go, but instead of pushing new entry into history stack, it replaces last

router.createMountEvent

Creates event that is triggered when particular route (optionally with particular params) is visited.

Can be useful to perform on mount logic

router.createMountEvent(['main', 'music']) // triggered when router.go(['main', 'music'])

router.createMountEvent({
  routes: ['main', 'music'],
  params: { musicId: 5 }
}) // triggered only when visited with particular params

router.createUnmountEvent

It has same signatures as createMountEvent, but gets triggered when you leave particular route (optionally with particular params)

router.createParamStore

Create store that holds value of particular param, gets updated when that param is changed

const $userId = createParamStore('userId');

See Working with params

DOM API

DOM API is separated from core to its own module at effing-router/dom

bindDom(router, routesList, basename = '')

Accepts router instance. It will synchronize router state with browser url. It will have no effect in non-browser environments.

React API

API for rendering routes as React components, it's located at effing-router/react

<RouterView router routesList/>

Renders route tree

childRoute()

Each rendered component gets passed a childRoute function as prop, it is used to render child routes, and you can also pass additional props inside

const Main = ({ childRoute }) => {
  return <div>
    Main route
    { childRoute({ someExtraProp: 3 }) }
  </div>
}

useRouter()

Hook for accessing current router state. Also can be useful for conditional rendering

import { useRouter } from 'effing-router/react';

const Users = ({ childRoute }) => {
  const { path, params } = useRouter();

  return <div>
    User id: { params.userId }
    {path.includes('news') && <News/>}
  </div>
}

License

MIT

About

Config-based router with support of effector as first-class citizen

Resources

License

Stars

Watchers

Forks

Packages

No packages published