loading...
Cover image for Use next.js with react-router

Use next.js with react-router

toomuchdesign profile image Andrea Carraro Updated on ・6 min read

This repo documents an attempt of using Next.js (preserving native SSR features) with the following setup:

This document is available as:


Disclaimers

  • Next.js team strongly advises against this approach.
  • This experiment was carried out at the times of Next.js v9.3: the framework has changed a lot since then.

Part one, basic setup

1 - Install Next.js

Relevant repo commit.

Install NextJS as usual and create the single entry point file at pages/index.js.

2 - Redirect all requests to single entrypoint

Relevant repo commit.

In order to skip file system-based routing, we'll configure a custom Next.js server to forward all the requests to our single entrypoint.

We'll use Next.js Server.render method to render and serve the entrypoint.

// server.js
const express = require('express');
const nextJS = require('next');

async function start() {
  const dev = process.env.NODE_ENV !== 'production';
  const app = nextJS({dev});
  const server = express();
  await app.prepare();

  // Redirect all requests to main entrypoint pages/index.js
  server.get('/*', async (req, res, next) => {
    try {
      app.render(req, res, '/');
    } catch (e) {
      next(e);
    }
  });

  server.listen(3000, err => {
    if (err) throw err;
    console.log(`> Ready on http://localhost:3000`);
  });
}

start();
Enter fullscreen mode Exit fullscreen mode

Run the dev server, and the entrypoint page at pages/index.js should be served as response for any requested url. 👊

3 - Introduce react-router

Relevant repo commit.

In order to get different responses according to the requested url we need a routing system.

We'll use react-router (see it's docs about SSR) and wrap the application with a StaticRouter or a BrowserRouter based on the environment application environment (server or browser).

Install react-router and react-router-dom:

npm i react-router react-router-dom -S
Enter fullscreen mode Exit fullscreen mode

...and update the pages/index.js entrypoint to use some Link and Route components from react-router-dom (see repo).

Let's now declare a withReactRouter HOC to wrap the application with the proper router:

// next/with-react-router.js
import React from 'react';
import {BrowserRouter} from 'react-router-dom';
const isServer = typeof window === 'undefined';

export default App => {
  return class AppWithReactRouter extends React.Component {
    render() {
      if (isServer) {
        const {StaticRouter} = require('react-router');
        return (
          <StaticRouter
            location={this.props.router.asPath}
          >
            <App {...this.props} />
          </StaticRouter>
        );
      }
      return (
        <BrowserRouter>
          <App {...this.props} />
        </BrowserRouter>
      );
    }
  };
};
Enter fullscreen mode Exit fullscreen mode

...and wrap the application with withReactRouter HOC:

// pages/_app.js
import App, {Container} from 'next/app';
import React from 'react';
import withReactRouter from '../next/with-react-router';

class MyApp extends App {
  render() {
    const {Component, pageProps} = this.props;
    return (
      <Container>
        <Component {...pageProps} />
      </Container>
    );
  }
}

export default withReactRouter(MyApp);
Enter fullscreen mode Exit fullscreen mode

Run the dev server, and you should be able to see your routes live and server side rendered.

Part two, context information

One of my favourite react-router features consists of the possibility of adding context information during the rendering phase and returning server side responses based on the information collected into the context object.

This enables client side code to take control of the responses returned by the node server like returning a HTTP 404 instead of a "not found page" or returning a real HTTP 302 redirect instead of a client side one.

In order to achieve this behaviour we have to configure Next.js to do the following:

  1. render the requested page providing a context object to the app router
  2. check whether context object was mutated during the rendering process
  3. decide whether to return the rendered page or do something else based on context object

4 - Provide context object to the router

Relevant repo commit.

We'll inject an empty context object into Express' req.local object and make it available to the router application via React Context.

Let's inject context object into Express' req.local object:

// server.js
server.get('/*', async (req, res, next) => {
  try {
+   req.locals = {};
+   req.locals.context = {};
    app.render(req, res, '/');
Enter fullscreen mode Exit fullscreen mode

Next.js provides a req and res objects as props of getInitialProps static method. We'll fetch req.originalUrl and req.locals.context and handle it over to the static router.

// next/with-react-router.js
  return class AppWithReactRouter extends React.Component {
+   static async getInitialProps(appContext) {
+     const {
+       ctx: {
+         req: {
+           originalUrl,
+           locals = {},
+         },
+       },
+     } = appContext;
+     return {
+       originalUrl,
+       context: locals.context || {},
+     };
+   }

  // Code omitted
          <StaticRouter
-           location={this.props.router.asPath}
+           location={this.props.originalUrl}
+           context={this.props.context}
          >
Enter fullscreen mode Exit fullscreen mode

5 - Separate rendering and response

Relevant repo commit.

Since we want to provide extra server behaviours based on req.locals.context in-between SSR and server response, Next.js Server.render falls short of flexibility.

We'll re-implement Server.render in server.js using Next.js Server.renderToHTML and Server.sendHTML methods.

Please note that some code was omitted. Refer to the source code for the complete implementation.

// server.js
  server.get('/*', async (req, res, next) => {
    try {
+     // Code omitted

      req.locals = {};
      req.locals.context = {};
-     app.render(req, res, '/');
+     const html = await app.renderToHTML(req, res, '/', {});
+
+     // Handle client redirects
+     const context = req.locals.context;
+     if (context.url) {
+       return res.redirect(context.url)
+     }
+
+     // Handle client response statuses
+     if (context.status) {
+       return res.status(context.status).send();
+     }
+
+     // Code omitted
+     app.sendHTML(req, res, html);
    } catch (e) {
Enter fullscreen mode Exit fullscreen mode

Before sending the response with the rendered HTML to the client, we now check the context object and redirect or return a custom HTTP code, if necessary.

In order to try it out, update the pages/index.js entrypoint to make use of <Redirect> and <Status> components and start the dev server.

Summary

We showed how it's be possible to setup Next.js take full advantage of react-router, enabling single entrypoint approach and fully preserving SSR.

In order to do so we:

  1. Redirected all server requests to a single entrypoint
  2. Wrapped the application (using HOC) with the proper react-router router
  3. Injected req server object with a locals.context object
  4. Provided HOC wrapper with req.locals.context and req.originalUrl
  5. Extended next.js Server.render to take into account req.locals.context before sending HTML

The re-implementation of Server.render in userland code is the most disturbing part of it, but it might be made unnecessary by extending a bit Server.render API in Next.js.

Results

react-router rendered server side

react-router's <Route> components get statically rendered on the server based on received req.originalUrl url.

Server side render

HTTP 302 redirect triggered by client code

When server rendering process encounters <Redirect from="/people/" to="/users/" /> component, the server response will return an HTTP 302 response with the expected Location header.

HTTP 302 redirect

HTTP 404 triggered by client code

When server rendering process encounters <Status code={404}/> component, the server response returns an HTTP response with the expected status code.

HTTP 404 redirect

Further consideration

I'm sure this setup is way far from being optimal. I'll be happy take into account any suggestions, feedbacks, improvements, ideas.

Issues

  • Static pages not being exported
  • Dev mode cannot build requested route on demand
  • getInitialProps not implemented

Discussion

pic
Editor guide
Collapse
ssdh233 profile image
ssdh233

Hello Andrea,
I tried to do the same thing but just felt I shouldn't use next.js if I don't let it handle the routing and SSR things. How do you think about it? Could you still feel any benefits of using next.js after doing this?

Collapse
tclain profile image
Timothée Clain

In my own experience in a large entreprise express app, sometimes you cannot afford to migrate your app to next all at once, but you still need to ssr some pages. The solution might not seem sexy, but it can be a path in the scenario

Collapse
arnemahl profile image
Arne Mæhlum

This was our case as well, except multiple apps owned by different teams. The fact that you can use react-router inside a Next app means one less hurdle to overcome, which makes it easier to get buy-in from each team.

Getting more pages together in the same app makes it so much more productive to share code across pages 😁

Collapse
toomuchdesign profile image
Andrea Carraro Author

That was exactly my case.

Collapse
toomuchdesign profile image
Andrea Carraro Author

Given the sour reaction I had from one of next.js maintainers for documenting this approach, I would definitely suggest to use next.js only the way they want it to be used.

I wouldn't spend anymore time to provide feedbacks to the project.

Collapse
tclain profile image
Timothée Clain

What do you mean "a sour reaction" ? That's seem to be pretty intense reaction for a simple blog post. I guess that we are all free as engineers to share approach and ideas. If an approach is not efficient enough, or whatever disagreement it can be, we should stay no matter what respectful and answer only based on facts. anyway keep up the good work.

Thread Thread
mikestopcontinues profile image
Mike Stop Continues

Zeit's business model is in hosting Next.js sites, so it's in their best interest to reduce the customization they allow to end users. Tons of people would rather use react-router (just see the github issues), and it's entirely possible to preserve all the other next features in doing so. But supporting such a different way of routing would mean way more headaches for their business than they seem to think is worth it. I don't blame them, but I also prefer "toolkit"-style libraries over opinionated monoliths, so I'm trying to move away from Next after a brief experiment while react-static was on hiatus.

Collapse
8kigai profile image
8kigai

Hi, is that a typo in the static router closing tag in // next/with-react-router.js ??

</StaticRouterlocation={this.props.router.asPath}>

And also I am wondering what is the benefit of using react router instead of next/router?

Collapse
toomuchdesign profile image
Andrea Carraro Author

Hi @8kigai , yep, definitely a typo, thanks for pointing out.
The 2 routing systems take 2 very different approaches. There's not best one but just tradeoffs.

In my last project we had an architectural setup which made very hard to write custom server configuration to properly handle custom url parameters. They come for almost free with react-router, for example.

The codebase was also very bound to react-router's ability of rendering nested Route components instead of relying on file system routing.

Collapse
arthkun profile image
Arthur Cougé

Hello Andrea, nice article!
I'm only discovering the ssr with next and I was wondering, is it better to wrap the whole App like you do in react-router, even if I only use it for few pages, or is it better to wrap only the pages and component I use with react-router the same way I wrap only with redux pages with redux?