Client-Side and Server-Side Redirects in Next.js
Tags: nextjs, redirect, ssr, react
Published: 2020-04-28 15:18:54

> This guide was written back when only `getInitialProps` existed and only applies to that method. This does not apply to `getServerSideProps`, and it doesn't work with `getServerSideProps` because you can't use both `getInitialProps` and `getServerSideProps` on the same page.

Sometimes when rendering, you might want to perform a redirect. For example, you might have an HOC that only renders the component when the user is authenticated and otherwise redirects to the login page. Next.js supports both client-side and server-side rendering (SSR) and unfortunately the method for redirecting is very different in both contexts.

## Client-Side

Client-side, imperative navigation is done via [next/router](https://nextjs.org/docs/api-reference/next/router).

```js
import Router from 'next/router'

Router.push('/new/url')
```

There's also a `useRouter()` hook that can be used in components.

```js
import { useRouter } from 'next/router'

function RedirectPage() {
   const router = useRouter()
   // Make sure we're in the browser
   if (typeof window !== 'undefined') {
     router.push('/new/url')
   }
}

export default RedirectPage
```

## Server-Side

Router uses `window.history` underneath which means you can't change URLs on the server. Instead, we must get access to the response object and respond with an HTTP redirect status code.

The response object is available via the [context object](https://nextjs.org/docs/api-reference/data-fetching/getInitialProps#context-object) that get's passed to `getInitialProps()`.

```js
import { useRouter } from 'next/router'

function RedirectPage({ ctx }) {
  const router = useRouter()
  // Make sure we're in the browser
  if (typeof window !== 'undefined') {
    router.push('/new/url');
    return; 
  }
}

RedirectPage.getInitialProps = ctx => {
  // We check for ctx.res to make sure we're on the server.
  if (ctx.res) {
    ctx.res.writeHead(302, { Location: '/new/url' });
    ctx.res.end();
  }
  return { };
}

export default RedirectPage
```

## Do Both in an HOC

That's messy logic for a page component, and if we plan on doing redirects in more than one place then it'd be best to abstract that into an HOC component.

```js
import { useRouter } from 'next/router';

function isBrowser() {
  return typeof window !== 'undefined';
}

/**
 * Support conditional redirecting, both server-side and client-side.
 *
 * Client-side, we can use next/router. But that doesn't exist on the server.
 * So on the server we must do an HTTP redirect. This component handles
 * the logic to detect whether on the server and client and redirect
 * appropriately.
 *
 * @param WrappedComponent The component that this functionality
 * will be added to.
 * @param clientCondition A function that returns a boolean representing
 * whether to perform the redirect. It will always be called, even on
 * the server. This is necessary so that it can have hooks in it (since
 * can't be inside conditionals and must always be called).
 * @param serverCondition A function that returns a boolean representing
 * whether to perform the redirect. It is only called on the server. It
 * accepts a Next page context as a parameter so that the request can
 * be examined and the response can be changed.
 * @param location The location to redirect to.
 */
export default function withConditionalRedirect({
  WrappedComponent,
  clientCondition,
  serverCondition,
  location
}) {
  const WithConditionalRedirectWrapper = props => {
    const router = useRouter();
    const redirectCondition = clientCondition();
    if (isBrowser() && redirectCondition) {
      router.push(location);
      return <></>;
    }
    return <WrappedComponent {...props} />;
  };

  WithConditionalRedirectWrapper.getInitialProps = async (ctx) => {
    if (!isBrowser() && ctx.res) {
      if (serverCondition(ctx)) {
        ctx.res.writeHead(302, { Location: location });
        ctx.res.end();
      }
    }

    const componentProps =
      WrappedComponent.getInitialProps &&
      (await WrappedComponent.getInitialProps(ctx));

    return { ...componentProps };
  };

  return WithConditionalRedirectWrapper;
}
```

We added some logic to add a condition on the redirect, and now it's getting a little ugly, but that HOC lets us make other conditional redirecting HOCs that are much more simple. Let's say we want to create a `withAuth()` HOC that redirects the user to the login page if they aren't already logged in.

```js
// This is a hook that returns a simple boolean: true if the user is
// signed in, false otherwise.
import { useIsAuthenticated } from 'src/providers/Auth';
import withConditionalRedirect from '../withConditionalRedirect';

/**
 * Require the user to be authenticated in order to render the component.
 * If the user isn't authenticated, forward to the signin page.
 */
export default function withAuth(WrappedComponent) {
  return withConditionalRedirect({
    WrappedComponent,
    location: '/signin',
    clientCondition: function withAuthClientCondition() {
      return !useIsAuthenticated();
    },
    serverCondition: function withAuthServerCondition(ctx) {
      // This isn't a good way to check for cookie values.
      // See the blog post linked below for something better.
      // We kept it simple here.
      return !ctx.req.headers.cookie.includes('session');
    }
  });
}
```

> For more details on handling authentication in SSR with Next.js, read [Detecting Authentication Client-Side in Next.js with an HttpOnly Cookie When Using SSR](https://dev.to/justincy/detecting-authentication-client-side-in-next-js-with-an-httponly-cookie-when-using-ssr-4d3e).

Why do we keep `clientCondition` and `serverCondition` separate? They are run in very different contexts: `clientCondition` is run during the component rendering and can use hooks while `serverCondition` is run in `getInitialProps()`, has access to `ctx` (and thereby `req` and `res`), and can't use hooks because it's not part of the component render.

You might wonder why we don't just return `ctx` from `getInitialProps()`. I tried it. It doesn't work because `req` and `res` are circular structures and can't be serialized into JSON to send down to the client for hydrating. See [Circular structure in "getInitialProps" result](https://github.com/zeit/next.js/blob/master/errors/circular-structure.md).