@interdan/conditional-router
TypeScript icon, indicating that this package has built-in type declarations

1.0.19 • Public • Published

Conditional react/redux router

Library allows to setup routing for React-Redux application. Main features:

  • clean and simple setup with pure JavaScript (TypeScript)

  • every route rule allows to perform checks and, if necessary, redirection: you don't need <Redirect ... /> inside your components

  • when some route is applied you can access redux state, routing parameters and dispatch before your component even start mounting; so when a component is being connected to store - you can be sure all the selectors have returned the correct values (based on your current route and routing parameters)

  • with requiredStep and routesToLockOnFirstLoad options performs auto-redirection back and forward in your business flow

  • supports multi-domains simulation: drastically simplify your development when you are building the solution that will be published with several different domain names; on your single localhost you can jump across all your domains and see how the solution will behave with real domain names

  • minor dependencies: path-to-regexp and qs only; also require you to pass history object (the most popular choise is to use history npm package)

Installation

npm install @interdan/conditional-router

Peer dependencies

Library expects your app is using react with redux, it's a list of peer dependencies:

  • react, react-dom: ^16.7.0

  • redux - ^4.0.1, react-redux - ^6.0.0

The best way to get the filing how it works - looks at the example app in git repository.

Simple example

Creating a history object in separate file (to support hot module replacement):

import { createBrowserHistory } from 'history';
export default createBrowserHistory();

Your app component is fairly simple:

import React from 'react';
import { Provider } from 'react-redux';
import history from './sharedHistory';
import { ConditionalRouter } from '@interdan/conditional-router';
import store from '<your redux store path>/store';
import setRoutingRules from './routingRules';

const App = () => (
  <Provider store={store}>
    <ConditionalRouter setRules={setRoutingRules} history={history} />
  </Provider>
);

Your routing rules file - (routingRules.js or routingRules.ts):

import StartView from './components/StartView';
import AboutUs from './components/AboutUs';
import Contacts from './components/Contacts';

export default function setRoutingRules(addRoutes, navigator) {
  addRoutes([
    {
      StartView,
      path: '/',
      useAsDefault: true,
    },

    { AboutUs },

    { Contacts },
  ]);
}

There is several options for navigation inside your components:

  • use button and handle click with method go of navigator object

  • use NavLink and pass there added navigator's route value

  • use NavLink and pass there strings like /, /AboutUs, /Contacts

  • in some other cases where we need a URL, we can get it just by url property of navigator object

In general, navigator.<RouteName>([optional parameters values]) returns UrlProvider object with two fields: url string property, and parameterless go() method.

Example shows how it works:

import React from 'react';
import navigator, { NavLink } from '@interdan/conditional-router';

const StartView = () => {
  const contactsUrl = navigator.Contacts().url;
  console.log(contactsUrl === '/Contacts'); // true

  return (
    <div className={start-view}>
      <h1>It is a test main page of our app</h1>
      Read "<NavLink to={navigator.AboutUs()}>about us</NavLink>" page.
      <NavLink to={navigator.AboutUs}>This link</NavLink> will also open "about us" page.
      Our contact information is available by <NavLink to="/Contacts">this link</NavLink>.

      Also you can reach the contacts page by clicking
      <button onClick={navigator.Contacts().go}>this button</button>
    </div>
  );
};

...

addRoutes function parameters

addRoutes it's a base function you will use to setup router with this library.

The first, second and all the next parameters except the last one are must be React components that you can use as a parent container. Let's say, you have such JSX:

<BrandContainer>
  <AboutUs />
  <Contacts />
</BrandContainer>

And you want to define the routes for this AboutUs and Contacts pages. You must put BrandContainer before the route rules:

addRoutes(BrandContainer, [
  { AboutUs },
  { Contacts },
]);

Last parameter passed to addRoutes function should be your route rule or an array of such rules. Two examples below create the same setup:

  addRoutes(BrandContainer, {
    StartView,
    path: '/',
    useAsDefault: true,
  });

  addRoutes(BrandContainer, { AboutUs });
  addRoutes(BrandContainer, { Contacts });

is the same as

addRoutes(BrandContainer, [
  {
    StartView,
    path: '/',
    useAsDefault: true,
  },

  { AboutUs },

  { Contacts },
]);

Route parameters

  • Required React component property - as you can guess from the example above, there are no other required parameters, except this one:
export default function setRoutingRules(addRoutes, navigator) {
  addRoutes([
    ...

    { Contacts },

    ...
  ]);
}

So this component is the only required property of the route rule object. Moreover, it can have any name, but the name must be stylized with Pascal case - the First letter should be in up case. Since all the other parameter names start with a lower case letter, it's possible to figure out where the component. The expected type of this parameter - a function or object (for memoized React components). This name also used to refer to this route using the navigator object: navigator.Contacts().url.

  • path property - allows you to override the default path name for this route or add parameters to your URL. The default value for path is /<Your component name>. So for example above path === '/Contacts'

To add path with parameters use colon with this format:

{ Contacts, path: `/Contacts/:country?cityName=:city` },

Parameters that are part of the path name are required, but query parameters are optional.

{ Contacts, path: `/Contacts/:country?cityName=:city` },

...

// next line will print: "url: /Contacts/Canada?cityName=Vancouver"
console.log('url: ', navigator.Contacts({ country: 'Canada', city: 'Vancouver' }).url);

...

<button onClick={navigator.Contacts({ country: 'Canada', city: 'Vancouver' }).go}>this button</button>
  • beforeRoute - is a function that accepts getState as a single parameter, must return another function that accepts an object - current URL match parameters. This function can return another route to perform the redirection or anything else to do nothing.

Example:

addRoute({
  Contacts,
  path: `/Contacts/:country?cityName=:city`,
  beforeRoute: getState => ({ country, city }) => {
      return !getState().session.isAuthorized && country === 'Oz' ? navigator.DontPlayWithUsPage : null;
    }
  },
});
  • onRoute - is a function that accepts two parameters: dispatch and getState, must return another function that accepts an object - current URL match parameters. This parameter can be used to dispatch actions once route applied but before mounting the corresponding component:
addRoute({
  Contacts,
  path: `/Contacts/:country?cityName=:city`,
  onRoute: (dispatch, getState) => ({ country, city }) => {
    if (getState().session.isAuthorized) {
      dispatch({ type: 'SET_CITY', payload: { country, city } });
    }
  },
});
  • useAsDefault (boolean parameter) - if true - sets that this route should be used for redirection when provided URL isn't valid. It could be 404 page, home page, etc. This parameter should be used only once per application, otherwise the exception will be thrown

  • requiredStep (boolean parameter) - if true - sets that this route is a part of the business workflow and it can't be skipped. The route added after this one will be checked that this route was visited, otherwise the redirected to this route will take place. First route always has true value as default for this parameter.

Example:

addRoute([
  {
    StartView,
    path: '/',
    useAsDefault: true,
    requiredStep: true,
  },

  { AboutUs, requiredStep: true },

  { Contacts },
]);

Then if user enter the URL in browser like my-site.com/Contacts without visiting my-site.com/AboutUs page he will be redirected to my-site.com/AboutUs or to my-site.com if he haven't visit start page either

  • routesToLockOnFirstLoad - an object with keys that are related to already added routes that will be locked for second visit when this route is applied:

Example:

addRoute([
  ...
  {
    Checkout,
    requiredStep: true,
  },
  {
    CongratulationPage,
    routesToLockOnFirstLoad: { Checkout, WirelessChargingChooser, WaterProtectionChooser, ... },
  },
]);

When a user reaches CongratulationPage he can't go back to Checkout, WirelessChargingChooser, WaterProtectionChooser and other locked routes.

  • alwaysAccessibleRoute - (boolean parameter) - if true all routing rules for other routes like requiredStep, routesToLockOnFirstLoad are ignored. The only possible redirection - if it set for the current rule with beforeRoute option. This option designed to simplify adding routes for general views: FAQ, Support, etc.

  • customSettings - an object that will be passed to the global onRoute handler:

addRoute([
  ...
  {
    Checkout,
    customSettings: { syncSession: true },
    requiredStep: true,
  },
  {
    CongratulationPage,
    customSettings: { syncSession: true },
    routesToLockOnFirstLoad: { Checkout, WirelessChargingChooser, WaterProtectionChooser, ResolutionChooser },
  },
]);

...

// 'dispatch' & 'getState' aren't used here, but you can get state value and dispatch actions
// 'routeParams' is also available here
const onRouteGlobalHandler = (dispatch, getState) =>
  (routeParams, routeCustomSettings) => {
    if (routeCustomSettings && routeCustomSettings.syncSession) {
      console.log('sync session');
    }
  };

...

<ConditionalRouter
  onRoute={onRouteGlobalHandler}
  ...
/>
  • skipWhenBackToPrevious - boolean parameter, equals false by default. Notify that specifit route must be skipped when navigator.navigateToPreviousRoute() method used

  • domainName - string parameter - make sense for multi-domains mode only, empty string or last provided domainName value in rules above. Bind specific route to specific domain name since it's possible to have several routes with the same route name like "Contacts", "AboutUs", etc.

Multi-domains mode

In this mode, your app with absolutely the same source code can be bound to different domain names. But for the local host, it will behave the same way as for real. Instead of different domain names, the router just adds domainName query parameters for all the URL and resolves them dynamically based on route rules and the current domain name. In this mode ConditionalRouter expects three more required parameter: isMultiDomainEmulating, useHttpsForMultiDomainsApp and switchStateActionCreator:

const isMultiDomainEmulating = isDevelopEnv || isStageEnv;

function switchStateActionCreator(domainName) {
  return {
    type: actionTypes.RESTORE_STATE,
    payload: restoreStateFromLocalStorage(domainName),
  };
}
...

 <ConditionalRouter
  isMultiDomainEmulating={isMultiDomainEmulating}
  useHttpsForMultiDomainsApp={isProduction || isStageEnv}
  switchStateActionCreator={switchStateActionCreator}

  setRules={setRoutingRules}
  history={history}
/>

It's not required for all cases buts it's a good practice to set domainName for all the route rules:

export default function setRoutingRules(addRoutes: TAddRoutesType, navigator: any) {

  setMainDomainRoutingRules(addRoutes, navigator);

  commonFlowDomains.forEach((domainName: string) => {
    addRoutes(BrandContainer, [
      {
        domainName,
        ResolutionChooser,
        ...
      },
      { WaterProtectionChooser, domainName, skipWhenBackToPrevious: true },
      {
        WirelessChargingChooser,
        domainName,
        ...
      },
    ]);
  });
}

Package Sidebar

Install

npm i @interdan/conditional-router

Weekly Downloads

45

Version

1.0.19

License

MIT

Unpacked Size

184 kB

Total Files

34

Last publish

Collaborators

  • interdan_developer
  • denis_oborskiy
  • olehrb