DEV Community

Justin
Justin

Posted on • Edited on

Client-Side and Server-Side Redirects in Next.js

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.

import Router from 'next/router'

Router.push('/new/url')
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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 that get's passed to getInitialProps().

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
Enter fullscreen mode Exit fullscreen mode

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.

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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');
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

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.

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.

Top comments (8)

Collapse
 
marvambi profile image
Marvin Ambrose

Hello @justincy,
This is a great resource that's helpful to many folks and I think you've done an awesome job with the SEO.
I'd recommend also that you update this redirection guide with reference to the new redirect() function from next/navigation for the server-side using the following:

import { redirect } from 'next/navigation'

and use any abstraction(s) over/atop redirect() as appropriate for the client side 😊.

Collapse
 
southclaws profile image
Barnaby

It seems this doesn't work now that getServerSideProps exists - if your page has SSP, Next will complain that you can't use getServerSideProps and getInitialProps.

Collapse
 
justincy profile image
Justin

You are correct in that you can't use both. This guide was back when only getInitialProps existed. I'll add a note to the top about that.

Collapse
 
southclaws profile image
Barnaby

I did figure it out though, thanks to the article I got the general idea of how it should work so thanks!

Thread Thread
 
atm profile image
atm

Did you do it per page? Or did you use SSP on an HOC?

Collapse
 
brennerspear profile image
Brenner

Hey Justin - I implemented the server-side version of your code. and it just returns me the json as opposed to redirecting me. Any thoughts on why?

Collapse
 
steveruizok profile image
Steve Ruiz

Any chance you're calling res.send rather than res.end?

Collapse
 
kwadoskii profile image
Austin Ofor

Thanks, the SS redirect helped me.