photo credit: Justin Luebke via unsplash

Authenticated Routing with React, React Router, Redux & TypeScript

Fred Rivett
Jul 18, 2019 · 14 min read

When creating a React application, we often have some form of authentication, with parts of the site designed for logged in users (e.g. a dashboard) and parts designed for logged out (e.g. a registration page). On top of these we have pages that are intended to be seen no matter the auth state of the user (e.g. terms and conditions).

This leaves us with three key types of page:

  • Routes only for logged in users
  • Routes only for logged out users
  • Routes for all users regardless of auth state

In this post I’ll share how we went about this at Octopus Wealth, utilising react-router-dom, react-redux & TypeScript.

By the end of this tutorial we’ll:

  • Have three types of routes: LoggedInRoute, LoggedOutRoute and Route
  • Auto redirect logged out users who visit LoggedInRoute, and vice-versa
  • Handle broken links with a 404 page
  • Have specific page sections for all LoggedInRoute pages, and the same for LoggedOutPages
  • Implement an example login system with localStorage
  • Dynamic global navigation based on auth state
  • Implement typing with TypeScript
The dummy app we’ll have built by the end (source code here)

Getting started

This post is based on using create-react-app with Typescript. To get started, run:

create-react-app your-app-name --typescript
cd your-app-name
yarn
yarn run start

See the Getting Started post from React for more info on create-react-app.

Implementing Redux foundations

For a web app to have authentication functionality, something needs to keep track of whether the user is logged in or not. This could be done with React’s Context API [e.g. this guide for an example on that], but as we had a need for Redux in our application we naturally decided to keep the auth state here too.

Simple Redux store state with Reducers

Redux’ official docs states that “Reducers specify how the application’s state changes in response to actions sent to the store. Remember that actions only describe what happened, but don’t describe how the application’s state changes.”

You’ll likely have a much more complex setup than this, utilising multiple reducers and combining them together, but to keep this simple, I’ve stripped it back just to what we need to get authentication working:

import { IAuthenticate, IUnauthenticate } from "../actions/current";
import { AUTHENTICATE, UNAUTHENTICATE } from "../constants";
import { ICurrent } from "../types";
export default function currentReducer(
state: ICurrent = {
uuid: null,
isAuthenticated: false,
},
action: IAuthenticate | IUnauthenticate,
): ICurrent {
switch (action.type) {
case AUTHENTICATE:
return {
...state, uuid: "placeholder-uuid", isAuthenticated: true
};
case UNAUTHENTICATE:
return { uuid: null, isAuthenticated: false }
}
return state;
}

This setup gives us our state that we’ll later provide down throughout our app.

With our reducer in place, we’ll setup our actions.

Adding our actions

Redux’ docs describe actions as “payloads of information that send data from your application to your store. They are the only source of information for the store.”

It’s here we setup functions that tell our store about our users authentication state:

import { ThunkDispatch as Dispatch } from "redux-thunk";import * as constants from "../constants";
import { ICurrent } from "../types";
export interface IAuthenticate {
type: constants.AUTHENTICATE;
}
export function authenticate(): IAuthenticate {
return {
type: constants.AUTHENTICATE,
};
}
export interface IUnauthenticate {
type: constants.UNAUTHENTICATE;
}
export function unauthenticate(): IUnauthenticate {
return {
type: constants.UNAUTHENTICATE,
};
}
export type AuthenticationAction = IAuthenticate | IUnauthenticate;

The two methods authenticate and unauthenticate simply update the Redux store. Later we’ll improve upon these to persist the users authentication state across sessions. At this stage the user will be logged out whenever the site is loaded, so refreshes will cause the user to have to log in again.

Next up, constants.

Constants

Constants can seem a little unneccessary, and in smaller projects it’s a matter of preference as to whether they’re worth the little extra complexity they bring. At Octopus Wealth we utilise constants as they work nicely with TypeScript in catching typo’s. You can read Redux creator Dan Abramov’s thoughts on constants here.

export const AUTHENTICATE = "AUTHENTICATE";
export type AUTHENTICATE = typeof AUTHENTICATE;
export const UNAUTHENTICATE = "UNAUTHENTICATE";
export type UNAUTHENTICATE = typeof UNAUTHENTICATE;

Types

This is only necessary if you’re using TypeScript. Here we define the shape our store will take.

export interface ICurrent {
isAuthenticated: boolean;
uuid: string | null;
}

Basic Routing

Now we’ve got our base Redux setup in place, we can start implementing our route components. As we discussed at the start of the post, we want to have three route components by the end, LoggedInRoute, LoggedOutRoute, and the standard Route component.

LoggedInRoute (basic version)

Here we can define any custom components that all LoggedInRoute’s should show.

import * as React from "react";
import { Route } from "react-router-dom";
interface IProps {
exact?: boolean;
path: string;
component: React.ComponentType<any>;
}
const LoggedInRoute = ({
component: Component,
...otherProps
}: IProps) => (
<>
<header>
Logged In Header
</header>
<Route
render={otherProps => (
<>
<Component {...otherProps} />
</>
)}
/>
<footer>
Logged In Footer
</footer>
</>
);
export default LoggedInRoute;

LoggedOutRoute (basic version)

This is super similar to LoggedInRoute. As with that component, we can define custom components to only show to logged out users.

So far a user can still visit any page, regardless of auth state. We’ll implement auth state checks later on in this article.

import * as React from "react";
import { Route } from "react-router-dom";
interface IProps {
exact?: boolean;
path: string;
component: React.ComponentType<any>;
}
const LoggedOutRoute = ({
component: Component,
...otherProps
}: IProps) => (
<>
<header>
Logged Out Header
</header>
<Route
render={otherProps => (
<>
<Component {...otherProps} />
</>
)}
/>
<footer>
Logged Out Footer
</footer>
</>
);
export default LoggedOutRoute;

Pages

Our Pages component is where we list out all the routes we want to make available to our users, specifying which paths should lead to which components.

import * as React from "react";
import { connect } from "react-redux";
import { Route, Switch } from "react-router-dom";
import About from "../components/pages/About";
import Home from "../components/pages/Home";
import Landing from "../components/pages/Landing";
import LogIn from "../components/pages/LogIn";
import LogOut from "../components/pages/LogOut";
import NotFound from "../components/pages/NotFound";
import Terms from "../components/pages/Terms";
import LoggedInRoute from "../routes/LoggedInRoute";
import LoggedOutRoute from "../routes/LoggedOutRoute";
const Pages = () => {
return (
<Switch>
<LoggedOutRoute path="/" exact={true} component={Landing} />
<LoggedOutRoute path="/about" exact={true} component={About} />
<LoggedOutRoute path="/log-in" exact={true} component={LogIn} />
<LoggedInRoute path="/log-out" exact={true} component={LogOut} />
<LoggedInRoute path="/home" exact={true} component={Home} />
<Route path="/terms" exact={true} component={Terms} />
<Route component={NotFound} />
</Switch>
);
};
export default Pages;

In the example above we have a bunch of components, some just for logged out users, some for logged in users, and some that should be shown regardless of auth state.

Home (example page)

For simplicity of this demo, our pages are functional components that return some JSX.

You can get the code for the rest of the pages in the example repo.

import * as React from "react";const Home = () => (
<p>Logged in home page</p>
);
export default Home;

Log in page (basic version)

In this basic version of the log in page, implement the connect function from Redux, which consumes the store we’ll pass down via the Provider in index (see below) and passes through our actions as props ready to be dispatched. Clicking the log me in button will update our Redux store.

import * as React from "react";
import { connect } from "react-redux";
import { authenticate } from "../../actions/current";interface IProps {
authenticateConnect: () => void;
}
const LogIn = ({ authenticateConnect }: IProps) => (
<>
<p>Login page</p>
<button onClick={authenticateConnect}>log me in</button>
</>
);
const mapDispatchToProps = {
authenticateConnect: authenticate
};
export default connect(
null,
mapDispatchToProps,
)(LogIn);

Log out page (basic version)

Similar to our log in page, our log out page enables us to log the user out in our Redux store.

import * as React from "react";
import { connect } from "react-redux";
import { unauthenticate } from "../../actions/current";interface IProps {
unauthenticateConnect: () => void;
}
const LogOut = ({ unauthenticateConnect }: IProps) => (
<>
<p>Logout page</p>
<button onClick={unauthenticateConnect}>log me out</button>
</>
);
const mapDispatchToProps = {
unauthenticateConnect: unauthenticate
};
export default connect(
null,
mapDispatchToProps,
)(LogOut);

Nav (basic version)

Our global Nav component lets our users navigate around the site. For this simple version, users can hit any page, including a stray broken link, which should be caught by our 404 page, handled in the Pages component.

import * as React from "react";import { NavLink } from "react-router-dom";const Nav = () => (
<>
<ul>
<li>
<NavLink to="/">
Landing
</NavLink>
</li>
<li>
<NavLink to="/home">
Home
</NavLink>
</li>
<li>
<NavLink to="/about">
About
</NavLink>
</li>
<li>
<NavLink to="/terms">
Terms
</NavLink>
</li>
<li>
<NavLink to="/broken-link">
Broken link
</NavLink>
</li>
<li>
<NavLink to="/log-in">
Log in
</NavLink>
</li>
<li>
<NavLink to="/log-out">
Log out
</NavLink>
</li>
</ul>
</>
);
export default Nav;

App (basic version)

At this stage, our App contains our Router and outputs our Nav on each page.

import * as React from "react";
import { Route, Router } from "react-router-dom";
import history from "./history";
import Nav from "./components/Nav";
import Pages from "./routes/Pages";
const App = () => (
<Router history={history}>
<Nav />
<Route component={Pages} />
</Router>
);
export default App;

Index (basic version)

The index file is where it all gets going. Here we initialise React, enable Redux DevTools when not in production, and pass our Redux store down to any child component that wants to consume it.

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { compose, createStore } from "redux";
import "./index.css";
import App from "./App";
import currentReducer from "./reducers/current";
import * as serviceWorker from "./serviceWorker";
import { ICurrent } from "./types";
let composeEnhancers;if (
process.env.NODE_ENV !== "production" &&
(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
) {
composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
} else {
composeEnhancers = compose;
}
const store = createStore<ICurrent, any, any, any>(
currentReducer,
undefined,
composeEnhancers(),
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
serviceWorker.unregister();

History

The history file uses the history npm module to keep track of the browser history. Say history more, I hear you say?

// tslint:disable:interface-name
import { createBrowserHistory, Location } from "history";
declare global {
interface Window {
dataLayer: any;
}
}
const history = createBrowserHistory();export default history;

The good stuff

Now we’ve got our basics in place, let’s start wiring in the good stuff.

Reducer

So far our reducer has an initial state of false. This seems logical, assume that the user is logged out until we know otherwise.

In reality, we need to have confidence whether the user is logged out or whether we’ve not yet ascertained their auth state. Setting isAuthenicated to null by default enables us to have confidence that whenever isAuthenticated is true or false it is so because we’ve checked and explicitly know the users current auth state.

import { IAuthenticate, IUnauthenticate } from "../actions/current";
import { AUTHENTICATE, UNAUTHENTICATE } from "../constants";
import { ICurrent } from "../types";
export default function currentReducer(
state: ICurrent = {
uuid: null,
isAuthenticated: null,
},
action: IAuthenticate | IUnauthenticate,
): ICurrent {
switch (action.type) {
case AUTHENTICATE:
return {
...state, uuid: "placeholder-uuid", isAuthenticated: true
};
case UNAUTHENTICATE:
return { uuid: null, isAuthenticated: false }
}
return state;
}

Types

With this change, we need to update our types as isAuthenticated will no longer always be a boolean.

export interface ICurrent {
isAuthenticated: boolean | null;
uuid: string | null;
}

Actions with DB side effects

So far our actions simply update the Redux state. But any real application is going to need to persist the user login beyond one page load. You’ll want to do this on your server, but we can implement a simple local version with localStorage for the purposes of this demo.

Note: to be clear, you shouldn’t be storing any sensitive data (e.g. authentication) in local storage, it is not secure. Please use a secure auth setup for any app that goes into production.

To do this we implement two new functions, logIn and logOut.

As well as this, we add a new function checkAuthentication, which we’ll call upon initial load to either set the user as logged in or out depending on the state in localStorage.

import { ThunkDispatch as Dispatch } from "redux-thunk";import * as constants from "../constants";
import { ICurrent } from "../types";
export interface IAuthenticate {
type: constants.AUTHENTICATE;
}
function authenticate(): IAuthenticate {
return {
type: constants.AUTHENTICATE,
};
}
export interface IUnauthenticate {
type: constants.UNAUTHENTICATE;
}
function unauthenticate(): IUnauthenticate {
return {
type: constants.UNAUTHENTICATE,
};
}
export type AuthenticationAction = IAuthenticate | IUnauthenticate;export function logIn() {
return async (dispatch: Dispatch<AuthenticationAction, {}, any>) => {
await window.localStorage.setItem("authenticated", "true");
dispatch(authenticate());
};
}
export function logOut() {
return async (dispatch: Dispatch<AuthenticationAction, {}, any>) => {
await window.localStorage.setItem("authenticated", "false");
dispatch(unauthenticate());
};
}
export function checkAuthentication() {
return async (dispatch: Dispatch<AuthenticationAction, {}, any>) => {
const auth = await window.localStorage.getItem("authenticated");
const formattedAuth = typeof auth === "string" ?
JSON.parse(auth) :
null;
formattedAuth ? dispatch(authenticate()) : dispatch(unauthenticate());
};
}

Index

To get our new async logIn and logOut functions working, we need to use Redux Thunk’s middleware.

Redux Thunk’s docs explain the need for this well:

With a plain basic Redux store, you can only do simple synchronous updates by dispatching an action. Middleware extend the store’s abilities, and let you write async logic that interacts with the store.

Thunks are the recommended middleware for basic Redux side effects logic, including complex synchronous logic that needs access to the store, and simple async logic like AJAX requests.

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { applyMiddleware, compose, createStore } from "redux";
import thunkMiddleware from "redux-thunk-recursion-detect";
import "./index.css";
import App from "./App";
import currentReducer from "./reducers/current";
import * as serviceWorker from "./serviceWorker";
import { ICurrent } from "./types";
let composeEnhancers;if (
process.env.NODE_ENV !== "production" &&
(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
) {
composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
} else {
composeEnhancers = compose;
}
const store = createStore<ICurrent, any, any, any>(
currentReducer,
undefined,
composeEnhancers(applyMiddleware(thunkMiddleware)),
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
serviceWorker.unregister();

LogIn and LogOut pages

Next we need to pull through our logIn and logOut functions so our LogIn and LogOut pages include the localStorage side effects.

import * as React from "react";
import { connect } from "react-redux";
import { logIn } from "../../actions/current";interface IProps {
logInConnect: () => void;
}
const LogIn = ({ logInConnect }: IProps) => (
<>
<p>Login page</p>
<button onClick={logInConnect}>log me in</button>
</>
);
const mapDispatchToProps = {
logInConnect: logIn
};
export default connect(
null,
mapDispatchToProps,
)(LogIn);
import * as React from "react";
import { connect } from "react-redux";
import { logOut } from "../../actions/current";interface IProps {
logOutConnect: () => void;
}
const LogOut = ({ logOutConnect }: IProps) => (
<>
<p>Logout page</p>
<button onClick={logOutConnect}>log me out</button>
</>
);
const mapDispatchToProps = {
logOutConnect: logOut
};
export default connect(
null,
mapDispatchToProps,
)(LogOut);

With these changes in place, you should now be able to check localStorage and see this updating when you click the log me in and log me out buttons.

Updating localStorage when logging in/out

Auth check and loading state

Now we implement the checkAuthentication function into the App, so that on the initial load we can check whether the user has already logged into the app.

We also wait until isAuthenticated comes back either true or false before showing the app.

import * as React from "react";
import { connect } from "react-redux";
import { Route, Router } from "react-router-dom";
import history from "./history";
import Nav from "./components/Nav";
import Pages from "./routes/Pages";
import { checkAuthentication } from "./actions/current";
import { ICurrent } from "./types";
interface IProps {
checkAuthenticationConnect: () => void;
isAuthenticated: boolean | null;
}
const App = ({
checkAuthenticationConnect,
isAuthenticated

}: IProps) => {
React.useEffect(() => {
checkAuthenticationConnect();
}, []);
const app = isAuthenticated !== null ? (
<Router history={history}>
<Nav />
<Route component={Pages} />
</Router>
) : null;
return (
<div className="App">
{app}
</div>
);

}
const mapStateToProps = (state: ICurrent) => ({
isAuthenticated: state.isAuthenticated
});
const mapDispatchToProps = {
checkAuthenticationConnect: checkAuthentication
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(App);

Redirecting based on auth state

Finally we get to the crescendo of the matter, only allowing logged in users to see logged in routes, and vice-versa.

Whenever someone hits a LoggedInRoute, whether on first load or after navigating to the page, we’ll check if they’re logged in, and if they’re not, redirect them to the login page. And if they hit the LoggedOutRoute and are logged in, we’ll redirect them home.

import * as React from "react";
import { connect } from "react-redux";
import { Route } from "react-router-dom";
import history from "../history";
import { ICurrent } from "../types";
interface IProps {
exact?: boolean;
isAuthenticated: boolean | null;
path: string;
component: React.ComponentType<any>;
}
const LoggedInRoute = ({
component: Component,
isAuthenticated,
...otherProps
}: IProps) => {
if (isAuthenticated === false) {
history.push("/log-in");
alert("this is a logged in route, you are logged out, redirected to log in");
}
return (
<>
<header>
Logged In Header
</header>
<Route
render={otherProps => (
<>
<Component {...otherProps} />
</>
)}
/>
<footer>
Logged In Footer
</footer>
</>
);
};
const mapStateToProps = (state: ICurrent) => ({
isAuthenticated: state.isAuthenticated
});
export default connect(
mapStateToProps
)(LoggedInRoute);
import * as React from "react";
import { connect } from "react-redux";
import { Route } from "react-router-dom";
import history from "../history";
import { ICurrent } from "../types";
interface IProps {
exact?: boolean;
isAuthenticated: boolean | null;
path: string;
component: React.ComponentType<any>;
}
const LoggedOutRoute = ({
component: Component,
isAuthenticated,
...otherProps
}: IProps) => {
if (isAuthenticated === true) {
history.push("/home");
alert("this is a logged out route, you are logged in, redirected to home page");
}
return (
<>
<header>
Logged Out Header
</header>
<Route
render={otherProps => (
<>
<Component {...otherProps} />
</>
)}
/>
<footer>
Logged Out Footer
</footer>
</>
);
};
const mapStateToProps = (state: ICurrent) => ({
isAuthenticated: state.isAuthenticated
});
export default connect(
mapStateToProps
)(LoggedOutRoute);

Dynamic nav

The finishing touch here is tweaking the navigation to be relevant to the users auth state.

And for the purpose of making it clear in this demo, we output the users auth state in the nav too.

import * as React from "react";
import { connect } from "react-redux";
import { NavLink } from "react-router-dom";
import { ICurrent } from "../types";
interface IProps {
isAuthenticated: boolean | null;
uuid: string | null;
}
const Nav = ({ isAuthenticated, uuid }: IProps) => {
const logInOut = isAuthenticated ? (
<li>
<NavLink to="/log-out">
Log out
</NavLink>
</li>
) : (
<li>
<NavLink to="/log-in">
Log in
</NavLink>
</li>
);
const mainLinks = isAuthenticated ? (
<li>
<NavLink to="/home">
Home
</NavLink>
</li>
) : (
<>
<li>
<NavLink to="/">
Landing
</NavLink>
</li>
<li>
<NavLink to="/about">
About
</NavLink>
</li>
</>
);
return (
<>
<p>Auth state: {isAuthenticated ? `Logged in user: ${uuid}` : "Logged out"}</p>
<ul>
{mainLinks}
<li>
<NavLink to="/terms">
Terms
</NavLink>
</li>
<li>
<NavLink to="/broken-link">
Broken link
</NavLink>
</li>
{logInOut}
</ul>
</>
);
};
const mapStateToProps = (state: ICurrent) => ({
uuid: state.uuid,
isAuthenticated: state.isAuthenticated,
});
export default connect(
mapStateToProps,
)(Nav);

The end product

Congrats for getting this far! It’s been a bit of a marathon, wiring up Redux, React Router, TypeScript and localStorage.

But all in, we’ve built a pretty good foundation for an app with authentication, logged in routes, logged out routes, and routes available regardless of the users auth state.

The only main part that you’d want to change for a production app is wiring up your proper authentication system instead of the dummy localStorage setup we’ve been working with here.

Hopefully the above has made sense. To get a better idea of it all working together, feel free to check out the example repo, clone it and play about, and ask any questions in the comments.

Octopus Wealth

Insights and updates from Octopus Wealth HQ.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store