Angular Style RouteGuard with react-router-dom

Moshiour Rahman
5 min readDec 13, 2020

This started as an R&D project,

I always liked the way how angular router works, it has tons of feature built into it, and RouteGuard is one of them, which I guess used most often.

So I thought I could create something similar in REACT also.

The idea is to develop a pattern that includes :

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

Let's create our RouterOutlet first

We will create a component that will take an Array of routes as input (More about Route later) before that let's create a definition file for typing purposes.

// **** routing.d.tsimport { ComponentType, LazyExoticComponent } from 'react';
import { Observable } from 'rxjs';


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

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

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

And then 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<RouteModel & RouteComponentProps> {
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<boolean> {
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 <Route {...rest} render={props => <Children {...props}/>}/>;
case RenderState.resolving:
return <p>Loading</p>;
case RenderState.notResolved:
return null;
}
}
}

const RenderedRoute = withRouter(RenderedRouteComponent);

const AppRouterOutlet = ({rootPath, routes = [], ...rest}: OutLetProps) => {
/**
* Resolve '/' mismatch in paths
* So that we dont 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 (
<Suspense fallback={<p>Loading</p>}>
<Switch>
{PARSED_ROUTES.map((route, index) => <RenderedRoute {...rest} {...route} key={index}/>)}
</Switch>
</Suspense>
);
};


export { AppRouterOutlet }

We will use our router outlet inside the `index.ts` file like this

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((
<BrowserRouter>
<AppRouterOutlet routes={APP_ROUTES}/>
</BrowserRouter>
), 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 we can see that AppRouterOutlet takes the APP_ROUTES as input, All the magic will happen inside our AppRouterOutlet.

Now we will create our 2nd Building block, Route

Every single route is an object that has the following properties

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

As we can see component property takes both ComponentType and LazyExoticComponent<any> as Input, which means our AppRouterOutlet can render both Lazy loaded component and Regular component which is awesome.

If any of us is familiar with angular then we know that Angular takes routes as an array and those routes contain guards, So I tried to keep the same configuration for our Route also except for some extra configuration which will be handy when using with 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]
}
];

Now the 3rd part, Guards,

In our project every Guard is a pure function, that takes some argument and returns one of

boolean | Promise<boolean> | Observable<boolean>

That means guard functional can make HTTP calls and other sorts of async tasks before resolving, this is very useful for things like checking auth in the server during navigation and many other things alike.

Here is an example guard function that returns observable 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, Full sample code is available at, https://github.com/mosh-dev/react-router-guard-example

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

THANK YOU, everyone, Happy Coding

--

--