Authentication with React-router 4.x

This article is inspired by the excellent tutorial by Scott Luptowski on how to add authentication to you React app. I attempt to re-invent the wheel again because the original article cites an older version of react-router and the instructions do not work if you are using react-router 4.x. There are a lot of breaking changes when you migrate from 3.x to 4.x, and there is an answer to all the whys here

Disclaimer

I’m not a Reactjs ninja or rockstar or paladin or anything of that sort. Just a dude with good intentions who had to spend an entire evening trying to figure out how authentication with react-router 4.x works, when the internet had only tutorials that use 3.x. So take my advice with a pinch of salt – I might not be following the best practices.

The goal

Your glorious new app requires the user to log in before they are allowed to do certain things. For example, if you were building the next Twitter, your users shouldn’t be able to tweet unless they are logged in. The idea here is to put certain URL patterns/pages behind an authentication wall so that if a user visits that page and the user is not logged in, he/she should be redirected to a login page. If the user is already logged in, proceed to show the requested content – and the user will have no idea about the karate chops we did behind the stage. Should the user try to navigate to a page that does not exist, we should show a 404 component as well.

The how-to

The solution is simple enough. Just like Scott explained in the original article, we create a React component that contains the login logic. This component wraps all of the routes that require authenticated users. Our entry point to the app would look something like this:

ReactDOM.render(
  <Router>
	<App />
  </Router>,
  document.getElementById('app')
);

But where did all the routes go? From react-router 4.x, you don’t get to define all your routes in one place. Yep, you read that right. So our Appcomponent will be doing its part in routing:

<pre class="wp-block-syntaxhighlighter-code brush: jscript; notranslate">class App extends Component {
	constructor(props){
		super(props);
	}

	render() {
		return (
			<div>
				All the awesomeness in the world converged to a single component.
				
					
  					
  				
			</div>
		
		)
	}
}</pre>

So what are we doing here? If the url exactly matches /, we render a Homecomponent. For everything else that is a subset of /, we render RootRouteWrapper which will subsequently route our requests. So all the other url patterns (eg: /pizza/pizza/yummy) would go on to render the RootRouteWrapper component. But what’s that Switch component doing there? If we had not enclosed the routes in a Switch, react-router would have rendered all routes that matched the url. So if the user visits your-awesome-app.com, all the routes for / will trigger – both Home and RootRouteWrapper! If your routes are enclosed in Switch, react-router will render only the first match – in our example the Home component.

OK. So now we can show a home page. What does the RootRouteWrappercomponent do again?

<pre class="wp-block-syntaxhighlighter-code brush: jscript; notranslate">class RootRouteWrapper extends Component {
	render() {
		return (
			<div id="RootRouteWrapper">
				
					
					
					
				
			</div>
		)
	}
}</pre>

We define 2 routes here – /login to show the user a login prompt and /tweet to let the user post a tweet. Now /tweet should be accessible only if the user is logged in. EnsureLoggedInContainer is the magic component that will handle the login logic for us. The idea is to configure all routes that need authentication to render the EnsureLoggedInContainer. You can also see that we have defined a route that will render the PageNotFoundcomponent if the URL does not match any configured routes. On to our login logic:

import {Route, Switch, withRouter} from 'react-router-dom';

class EnsureLoggedInContainer extends Component {
	constructor(props){
		super(props);
	}

	componentDidMount(){
		const {dispatch, currentURL, isLoggedIn} = this.props;

		if(!isLoggedIn){
			this.props.history.replace('/login');
		}
	}

	render() {
		const {isLoggedIn} = this.props;

		return (
			<Switch>
				<Route path="/tweet" component={Tweet} />
			</Switch>
		)

	}
}


export default withRouter(EnsureLoggedInContainer);

The assumption is that the Tweet component shows the user an input box to type a message. Notice how we have declared a Route for /tweet again inside the EnsureLoggedInContainer. When the user navigates to /tweetRootRouteWrapper renders EnsureLoggedInContainer which in turn renders Tweet. If the user is not logged in, componentDidMount will redirect the user to the login page. Remember that you need to export the class with withRouter for the history to be available in the props. Also, you would need to maintain the state of the application separately – this article assumes that you have laid down the necessary plumbing to pass isLoggedIn as a prop to EnsureLoggedInContainerisLoggedIn should come from your application state – and react-redux seems to be the most popular choice here. How to use react-redux to pass properties to your component is beyond the scope of this article. If you are interested, there’s a really good introduction here

In case you wanted to add another page that displays a tweet – say /tweet/1– that would show the tweet with id 1 in a TweetContainercomponent – you would have to write the necessary routing logic inside the Tweet component. /tweet/:id would automatically require authentication since its parent route – /tweet – renders EnsureLoggedInContainer.

Caveats

You have to make changes at 2 places to add a new route that needs authentication – in the RootRouteWrapper component and then again in EnsureLoggedInContainer. I wonder if there is a more elegant solution

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s