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
androutesToLockOnFirstLoad
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
andqs
only; also require you to pass history object (the most popular choise is to usehistory
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
ofnavigator
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 ofnavigator
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 forpath
is/<Your component name>
. So for example abovepath === '/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 likerequiredStep
,routesToLockOnFirstLoad
are ignored. The only possible redirection - if it set for the current rule withbeforeRoute
option. This option designed to simplify adding routes for general views: FAQ, Support, etc. -
customSettings
- an object that will be passed to the globalonRoute
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, equalsfalse
by default. Notify that specifit route must be skipped whennavigator.navigateToPreviousRoute()
method used -
domainName
- string parameter - make sense for multi-domains mode only, empty string or last provideddomainName
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,
...
},
]);
});
}