tutorial // Apr 07, 2021

How to Handle Authenticated Routes with Next.js

How to create a HOC (higher-order component) that can conditionally redirect a user based on their logged in or logged out status.

How to Handle Authenticated Routes with Next.js

In Next.js, by default, all of your routes are treated the same.

While your specific app may include pages or routes that are intended only for logged-in users, out of the box, Next.js does not provide a way to isolate these pages based on a user's authentication status.

This is expected as Next.js is designed to handle a simple, well-defined set of tasks. While it can be used as a front-end for an application—like in CheatCode's Next.js boilerplate—traditionally it's used to generate static marketing sites or sites backed by a headless CMS.

Fortunately, solving this problem isn't too complex. To solve it, we're going to implement two components:

  1. authenticatedRoute which will be a function that returns a React component wrapped with a conditional check for the user's authentication status and a redirect if a user is unavailable.
  2. publicRoute which will be a function that returns a React component wrapped with a conditional check for the user's authentication status and a redirect if a user is present.

Implementing an Authenticated Route Component

First, let's build out the skeleton for our HOC and discuss how it's going to work:

/components/AuthenticatedRoute/index.js

import React from "react";

const authenticatedRoute = (Component = null, options = {}) => {
 // We'll handle wrapping the component here.
};

export default authenticatedRoute;

Here, we export a plain JavaScript function that takes two arguments: a React Component as the first argument and an object of options as the second. The Component represents the component of the protected page that we want to conditionally render.

When we go to put this to use, we'll do something like this:

/pages/<somePage>/index.js

import authenticatedRoute from '../../components/AuthenticatedRoute';

const MyComponent = () => {
  [...]
};

export default authenticatedRoute(MyComponent, { pathAfterFailure: '/login' })

Moving forward, let's populate our HOC with the core wrapper component:

/components/AuthenticatedRoute/index.js

import React from "react";

const authenticatedRoute = (Component = null, options = {}) => {
  class AuthenticatedRoute extends React.Component {
    state = {
      loading: true,
    };

    render() {
      const { loading } = this.state;

      if (loading) {
        return <div />;
      }

      return <Component {...this.props} />;
    }
  }

  return AuthenticatedRoute;
};

export default authenticatedRoute;

Here, we've populated our authenticatedRoute function's body with a class-based React component. The idea here is that we want to utilize the state and—next—the componentDidMount function for the class so that we can decide if we want to render the passed Component, or, redirect the user away from it.

/components/AuthenticatedRoute/index.js


import React from "react";
import Router from "next/router";

const authenticatedRoute = (Component = null, options = {}) => {
  class AuthenticatedRoute extends React.Component {
    state = {
      loading: true,
    };

    componentDidMount() {
      if (this.props.isLoggedIn) {
        this.setState({ loading: false });
      } else {
        Router.push(options.pathAfterFailure || "/login");
      }
    }

    render() {
      const { loading } = this.state;

      if (loading) {
        return <div />;
      }

      return <Component {...this.props} />;
    }
  }

  return AuthenticatedRoute;
};

export default authenticatedRoute;

Now, with our componentDidMount added, we can see our core behavior implemented. Inside, all we want to know is "is there a logged in user or not?" If there is a logged in user, we want to say "go ahead and render the passed Component." We can see this taking place down in the render() method of the AuthenticatedRoute component.

Here, we're saying, as long as loading is true, just return an empty <div /> (or, show nothing to the user). If we're not loading, just run the return statement at the bottom of the render().

What this achieves is saying "until we know that we have a logged in user, show nothing, and if we do have a logged in user, show them the page they're trying to access."

Back in componentDidMount() in the else statement, we're saying "okay, it doesn't look like the user is logged in so let's redirect them." To do the redirect in this example, we're using the built-in Next.js router to do the redirect for us, but you could use any JavaScript or React router you'd like (e.g., if we were using React Router we'd do this.props.history.push(options.pathAfterFailure || '/login').

Make sense? So, if we have a user, show them the component. If we don't have a user, redirect them to another route.

Determining logged-in status

Now, technically speaking, this is all we have to do. But you may be wondering "how do we know if the user is logged in?" This is where your own app comes in. In this example, we're using the CheatCode Next.js Boilerplate which relies on an authenticated user (if available) being present in a global Redux store.

To make this all a little more concrete, let's take a look at that setup now:

/components/AuthenticatedRoute/index.js

import React from "react";
import Router from "next/router";
import { connect } from "react-redux";

const authenticatedRoute = (Component = null, options = {}) => {
  class AuthenticatedRoute extends React.Component {
    state = {
      loading: true,
    };

    componentDidMount() {
      if (this.props.isLoggedIn) {
        this.setState({ loading: false });
      } else {
        Router.push(options.pathAfterFailure || "/login");
      }
    }

    render() {
      const { loading } = this.state;

      if (loading) {
        return <div />;
      }

      return <Component {...this.props} />;
    }
  }

  return connect((state) => ({
    isLoggedIn: state?.authenticated && !!state?.user,
  }))(AuthenticatedRoute);
};

export default authenticatedRoute;

The big change we've made here is to import the connect() method from the react-redux package (already installed in the boilerplate) and then call that function, passing it a mapStateToProps function and then wrapping it around our component. To be clear, this part:

/components/AuthenticatedRoute/index.js

return connect((state) => ({
  isLoggedIn: state?.authenticated && !!state?.user,
}))(AuthenticatedRoute);

Here, the function that we pass as the first argument to connect() is the mapStateToProps function (as it's named in the react-redux documentation). This function takes in the current global state for the application provided by the <ReduxProvider /> in /pages/_app.js in the CheatCode Next.js boilerplate.

Using that state, like the name implies, it maps that state to a React component prop that will be handed down to our <AuthenticatedRoute /> component defined just above it.

If we look close, here, we're setting a prop called isLoggedIn, checking to see if the authenticated value on our state is true and whether or not we have a user object on state. If we do? The user is logged in! If not, isLoggedIn is false.

If you look back in the componentDidMount() function, this is where we're putting the new isLoggedIn prop to use.

Using other authentication sources

If you're not using the CheatCode Next.js Boilerplate, how you get to your user's authenticated state depends on your app. A quick and dirty example of using another API would look something like this:

import React from "react";
import Router from "next/router";
import { connect } from "react-redux";
import { myAuthenticationAPI } from 'my-authentication-api';

const authenticatedRoute = (Component = null, options = {}) => {
  class AuthenticatedRoute extends React.Component {
    state = {
      loading: true,
    };

    async componentDidMount() {
      const isLoggedIn = await myAuthenticationAPI.isLoggedIn();

      if (isLoggedIn) {
        this.setState({ loading: false });
      } else {
        Router.push(options.pathAfterFailure || "/login");
      }
    }

    render() {
      const { loading } = this.state;

      if (loading) {
        return <div />;
      }

      return <Component {...this.props} />;
    }
  }

  return AuthenticatedRoute;
};

export default authenticatedRoute;

In this example, nearly everything is identical, but instead of anticipating an authentication value coming from a Redux store, we just call to our authentication API (e.g., Firebase) directly, relying on the return value to that call as our isLoggedIn status.

Implement a Public Route Component

Now, some good news: our publicRoute component is identical to what we looked at above with one tiny little change:

/components/PublicRoute/index.js

import React from "react";
import Router from "next/router";
import { connect } from "react-redux";

const publicRoute = (Component = null, options = {}) => {
  class PublicRoute extends React.Component {
    state = {
      loading: true,
    };

    componentDidMount() {
      if (!this.props.isLoggedIn) {
        this.setState({ loading: false });
      } else {
        Router.push(options.pathAfterFailure || "/documents");
      }
    }

    render() {
      const { loading } = this.state;

      if (loading) {
        return <div />;
      }

      return <Component {...this.props} />;
    }
  }

  return connect((state) => ({
    isLoggedIn: state?.authenticated && !!state?.user,
  }))(PublicRoute);
};

export default publicRoute;

Can you spot it? Up in the componentDidMount we've added a ! to say "if the user is not logged in, go ahead and render the component. If they are logged in, redirect them."

Literally the inverse logic to our authenticatedRoute. The point here is that we want to use the publicRoute() component on routes like /login or /signup to redirect already authenticated users away from those pages. This ensures we don't get database messes later like duplicate users or multiple user sessions.

Wrapping up

In this tutorial, we learned a simple pattern for creating a HOC (higer-order component) for redirecting users in our app based on their logged in (authentication) status. We learned how to implement the base component that "wraps" around the componetn we're trying to protect and how to implement the core logic to handle the render and redirect process.

We also looked at examples of using actual authentication data to add some context and clarify how this pattern can work in any authentication setup within Next.js.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode