Developer's Guide to Creating an Angular Style RouteGuard with React-router-dom - SELISE

Developer’s Guide to Creating an Angular Style RouteGuard with React-router-dom

January 25, 2021

Our team members are constantly striving to explore the frontiers of coding. The following guide has been developed by Moshiour Rahman, a SELISE team member. This was done in an attempt to replicate Angular Style RouteGuard in React.

I started this off as an R&D project, as I have always been fascinated with how Angular has helped me develop better frontend code. Armed with its ample in-built features, RouteGuard provides us with the best functionalities out there to manage user access rights on the navigation level

So, I thought I could replicate the system in REACT as well.
Hence, the idea to develop the following pattern took shape and that includes:

# A RouterOutlet like angular,
# Routes (array of Route)
# Guards

I have outlined 3 main steps below:

Step 1.
Let’s take a moment to create our RouterOutlet first. We will start with a component that will take an Array of routes as input (More about Route later) but before that let’s create a definition file for the purpose of typing.

// **** routing.d.ts
import { ComponentType, LazyExoticComponent } from ‘react’;
import { Observable } from ‘rxjs’;

export interface OutLetProps {
routes: RouteModel[];
rootPath?: string;
}

export interface RouteModel {
path: string;
component: ComponentType | LazyExoticComponent;
exact?: boolean;
title?: string;
guards?: any[];
}

export type GuardReturnType = boolean | Promise | Observable;
export type GuardProps = RouteModel & RouteComponentProps;

And then, follow it up with our Outlet component

import React, { Component, Suspense } from ‘react’;
import { Route, RouteComponentProps, Switch, withRouter } from ‘react-router-dom’;
import { ObjectMap } from ‘../../typings’;
import { from, Observable, Subject } from ‘rxjs’;
import { takeUntil } from ‘rxjs/operators’;
import { OutLetProps, RouteModel } from ‘./routing’;

enum RenderState {
resolved,
resolving,
notResolved
}

class RenderedRouteComponent extends Component {
state: ObjectMap = {
renderState: RenderState.resolving,
};
private componentDestroyed = new Subject();

componentDidMount(): void {
/**
* Lets run Guard Resolvers
*/
if (this.props.guards) {
from(this.resolveGuards(this.props.guards))
.pipe(takeUntil(this.componentDestroyed))
.subscribe(result => {
const renderState = result ? RenderState.resolved : RenderState.notResolved;
this.setState({renderState});
});
} else {
this.setState({renderState: RenderState.resolved});
}
}

componentWillUnmount(): void {
this.componentDestroyed.next();
this.componentDestroyed.complete();
}

private async resolveGuards(guards: any[]): Promise {
guards = […guards.reverse()];
return new Promise(resolve => {
const runGuards = async (guards) => {
/**
* Run guards One By One Using Recursive Call
*/
const guard = guards.pop();
if (guard) {
const result = guard(this.props);
switch (true) {
case typeof result === ‘boolean’:
result ? await runGuards(guards) : resolve(result);
break;
case result instanceof Promise:
const promiseResult = await result;
promiseResult ? await runGuards(guards) : resolve(promiseResult);
break;
case result instanceof Observable:
const obResult = await result.toPromise();
obResult ? await runGuards(guards) : resolve(obResult);
break;
}
} else {
/**
* When all Guards Passed or no guard
*/
resolve(true);
}
};
runGuards(guards);
});
}

/**
* Lazy Component DoesNot Work Directly in render Prop
* And We Should also Pass Props Given From React Router
* as we may need tem inside our Rendered Components
*/
render() {
const {component: Children, …rest} = this.props;
switch (this.state.renderState) {
case RenderState.resolved:
return }/>;
case RenderState.resolving:
return

Loading

;
case RenderState.notResolved:
return null;
}
}
}

const RenderedRoute = withRouter(RenderedRouteComponent);

const AppRouterOutlet = ({rootPath, routes = [], …rest}: OutLetProps) => {
/**
* Resolve ‘/’ mismatch in paths
* So that we don’t need to worry about putting ‘/’ before or after in our routes
*/
const PARSED_ROUTES = routes
.map((r: RouteModel) => ({…r, path: (rootPath ? rootPath + ‘/’ : ”) + r.path}))
.map((r: RouteModel) => ({…r, path: ‘/’ + r.path.split(/\/?\//).join(‘/’)}));
return (

Loading

}>

{PARSED_ROUTES.map((route, index) => )}

);
};

export { AppRouterOutlet }

For the `index.ts`, we will be using our router outlet in the subsequent way:

import React from ‘react’;
import { render } from ‘react-dom’;
import { BrowserRouter } from ‘react-router-dom’;
import { ServiceWorker } from ‘./serviceWorker’;
import { AppRouterOutlet } from ‘./app/routing/app-router-outlet.component’;
import { APP_ROUTES } from ‘./app/routing/app.routing’;

render((

), document.querySelector(‘#app’));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
ServiceWorker.unregister();

As you can see, the AppRouterOutlet considers the APP_ROUTES as an input. This AppRouterOutlet is where all the magic will take place.

Step 2
After successfully completing Step 1, we will move on to creating our 2nd Building block, Route. Every single route is an object that has the following properties:

interface RouteModel {
path: string;
component: ComponentType | LazyExoticComponent;
exact?: boolean;
title?: string;
guards?: any[];
}

As we can see, the component property considers both the ComponentType and LazyExoticComponent as Input, which means our AppRouterOutlet can render both the Lazy loaded component and the Regular component.

If any of us is familiar with angular, then we know that Angular takes routes as an array and these very routes tend to contain guards. As a result, I tried to maintain an identical configuration for our Route, with some exceptions. This will turn out to be handy when using the react-router-dom.

Here is our complete routes file,

import { lazy } from ‘react’;
import { asyncAuthGuardObservable } from ‘./guards/auth.guard’;
import { RouteModel } from ‘./routing’;
import { publicGuard } from ‘./guards/public.guart’;

export const APP_ROUTES: RouteModel[] = [
{
path: ”,
component: lazy(() => import(‘../pages/login/login-page.component’)),
exact: true,
guards: [publicGuard]
},
{
path: ‘dashboard’,
component: lazy(() => import(‘../pages/dashboard/dashboard-main.component’)),
guards: [asyncAuthGuardObservable]
}
];

Step 3
Now the 3rd part: Guards

In our project, every Guard is a pure function that tends to take on some form of argument and return one of the following:

boolean | Promise | Observable

That means guard functionality can make HTTP calls and other sorts of async tasks before resolving. This turns out to be useful when checking for auth in the server during navigation and similar situations.
Here is an example of a guard function that returns observable bits of boolean

/**
* Sample async Guards that returns an observable
* @param props
*/
export const asyncAuthGuardObservable = (props: GuardProps): GuardReturnType => {
return timer(2000).pipe(map(() => {
if (!loggedIn()) {
if (props.history) {
props.history.replace(‘/’);
}
return false;
}
return true;
}));
};

We can add as many Guards as possible to our Route. Here is a simple demo of the guard, the full sample code is available at, https://github.com/mosh-dev/react-router-guard-example
Demo: https://stackblitz.com/edit/react-ts-sjn9bv

Sign In will set a flag as Logged in, and redirect to the dashboard, which is a protected route. For the demo mentioned previously, the Root route is protected by a publicGuard, and the dashboard route is protected by authGuard. Additionally, the authGuard also redirects the user to the login page if not logged in. Same for the login page, if logged in, then publicGuard will redirect to the dashboard page.

Hope the steps can encourage you to try your hand at this. Thank you, everyone! Happy Coding!