HOC Authentication Redirects with Next.js, GraphQL, and Typescript

Published on 17 November 2019

This article describes how to redirect the user in Next.js based on whether the user is authenticated (logged in) using a properly typed Typescript React higher-order component (HOC).

This can extend to arbitrary conditions on the queried user, such as redirecting if the user does not have an active subscription.

The following Typescript module implements a general HOC that redirects based on authentication state by querying the user from the GraphQL backend. It then includes two examples:

  • If the user is not logged in, redirect to /login
  • If the user is logged in, redirect to /dashboard

Example HOC Usage

import { NextPage } from 'next';
// CHANGE THIS: Export to the appropriate path.
import { withLoginRedirect } from '../lib/auth';

const RequiresLoginPage: NextPage = () => {
  return <div>You're logged in</div>;
};

// If the user isn't logged in, this will redirect to `/login`.
export default withLoginRedirect(RequiresLoginPage);

Implementation

import React from 'react';
import { NextComponentType, NextPageContext } from 'next';
import Router from 'next/router';
import gql from 'graphql-tag';

/**
 * YOU DEFINE THESE.
 *
 * These variables should be set by you to query for the user and the types
 * appropriately. I recommend using GraphQL codegen
 * (https://graphql-code-generator.com/) to generate types from your schema and
 * queries from other files so that they can be used like so:
 *
 * import { User } from '../generated/apollo-client-types';
 * import {
 *   LoggedInUserDocument,
 * } from '../generated/apollo-client-types';
 *
 */
interface User {}
const LoggedInUserDocument = gql`<YOUR QUERY FOR USER OBJECT>`;

/**
 * A function that queries for the logged in user before rendering the page.
 * Should be called in getInitialProps. It redirects as desired.
 *
 * It allows for redirecting both if the user is not logged in (e.g., redirect
 * to login page) or redirecting if the user is logged in.
 *
 * If not logged in, redirects to the desired route.
 *
 * The return value indicates whether logic should continue or not after the
 * call.
 */
const redirectBasedOnLogin = async (
  ctx: NextPageContext,
  route: string,
  redirectIfAuthed: boolean
): Promise<boolean> => {
  const isLoggedIn = await ctx.apolloClient
    .query({
      query: LoggedInUserDocument,
      // Prevent caching issues when logging in/out without refresh.
      fetchPolicy: 'network-only',
    })
    .then(({ data }) => {
      if (!data || !data.loggedInUser) {
        return false;
      }
      return Boolean(data.loggedInUser);
    })
    .catch(() => {
      return false;
    });

  const shouldRedirect = redirectIfAuthed ? isLoggedIn : !isLoggedIn;
  if (shouldRedirect) {
    // https://github.com/zeit/next.js/wiki/Redirecting-in-%60getInitialProps%60
    if (ctx.res) {
      ctx.res.writeHead(302, {
        Location: route,
      });
      ctx.res.end();
    } else {
      Router.push(route);
    }
    return Promise.resolve(false);
  }
  return Promise.resolve(true);
};

/**
 * General HOC that allows redirection based on authentication. We should not
 * expose this: instead export specific routes and redirect combinations.
 */
const withAuthRedirect = (route: string, redirectIfAuthed: boolean) => <P,>(
  Page: NextComponentType<NextPageContext, {}, P>
) => {
  return class extends React.Component<P> {
    static async getInitialProps(ctx: NextPageContext) {
      const shouldContinue = await redirectBasedOnLogin(
        ctx,
        route,
        redirectIfAuthed
      );
      // Only continue if we're logged in. Otherwise, it might cause an
      // unnecessary call to a downstream getInitialProps that requires
      // authentication.
      if (!shouldContinue) {
        return {};
      }
      if (Page.getInitialProps) {
        return Page.getInitialProps(ctx);
      }
    }

    render() {
      return <Page {...this.props} />;
    }
  };
};

/**
 * HOC that redirects to login page if the user is not logged in.
 */
export const withLoginRedirect = withAuthRedirect('/login', false);

/**
 * HOC that redirects to the dashboard if the user is logged in.
 */
export const withDashboardRedirect = withAuthRedirect('/dashboard', true);

Found this helpful or have suggestions? Leave them in the comments below.

Typing Next.js getInitialProps HOCs is tricky, and I had trouble finding an example online.

Versions

At the time of writing, this was using Next 9.

Comments