React-router: [feature/documentation] v4 detect 404 in Server side rendering (or manage generic context data)

Created on 20 Mar 2017  路  4Comments  路  Source: ReactTraining/react-router

This is a feature request (or a documentation request in case this is already possible with current V4 codebase).

Basically there's no obvious way to detect when a route is not found in server side rendering , which would be useful to propagate the correct status code to the browser.

Let's show a use case to make this clear.

I have an App component which contains the following code:

// App.js
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { Menu } from './Menu';
import { Home } from './Home';
import { Blog } from './Blog';
import { About } from './About';
import { NotFound } from './NotFound';

export const App = () => (
  <div>
    <Menu />
    <hr />
    <Switch>
      <Route exact path="/" component={Home} />
      <Route path="/blog" component={Blog} />
      <Route path="/about" component={About} />
      <Route component={NotFound} />
    </Switch>
  </div>
);

export default App;

Where the last route is the one that works as a "not found" component.

In order to render this app from an express server I'd do the following:

// server-app.js
import path from 'path';
import { Server } from 'http';
import Express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import { App } from './components/App';

const app = new Express();
const server = new Server(app);

// use ejs templates
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

// define the folder that will be used for static assets
app.use(Express.static(path.join(__dirname, '..', 'assets')));

// universal routing and rendering
app.get('*', (req, res) => {
  const context = {};
  const markup = renderToString(
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>,
  );

  // context.url will contain the URL to redirect to if a <Redirect> was used
  if (context.url) {
    return res.redirect(302, context.url);
  }

  // TODO manage 404
  const status = 200; // or 404 is specific condition is met
  return res.status(status).render('index', { markup });
});

// start the server
const port = process.env.PORT || 3000;
const env = process.env.NODE_ENV || 'production';
server.listen(port, (err) => {
  if (err) {
    return console.error(err);
  }
  return console.info(`Server running on http://localhost:${port} [${env}]`);

Look closely at the TODO. It would be nice if there is an easy way to expose metadata to matched routes so that it could be possible to detect which route has been rendered and manage cases that, for example, can be considered as a 404.

The way I would see this working could be by adding some metadata in the routes like the following:

// App.js
// ...
export const App = () => (
  <div>
    <Menu />
    <hr />
    <Switch contextId="page">
      <Route exact path="/" component={Home} />
      <Route path="/blog" component={Blog} />
      <Route path="/about" component={About} />
      <Route component={NotFound} contextData={{ is404: true }} />
    </Switch>
  </div>
);
// ...

Note the contextId in the Switch component and the contextData in the last Route.

Then I would be using this metadata in my server side application as follows:

// server-app.js
// ...
const status = context.matchedRoutes.page.is404 ? 404 : 200;
// ...

Basically, the idea is that everytime renderToString is invoked, the context is populated with an object containing all the matched routes (mapped by contextId) and the relative contextData is used as a value for that route. In the case of the Switch component it will obtain the contextData of the matched route.

Does this approach make sense with the current architecture? Is there any sort of similar solution already available?

Thanks

Most helpful comment

Some better documentation of this should be added. For the time being, you'll have to do with this comment:

When a <Route> matches, the element it creates is passed four props: history, match, location, and staticContext. staticContext is the object that you passed to <StaticRouter>. This means that you can modify your <NotFound> component like so:

class NotFound extends React.Component {
  componentWillMount() {
    const { staticContext } = this.props;
    if (staticContext) {
      staticContext.is404 = true;
    }

  render() { ... }
  }
}

Alternatively, you can "render" a status. For example, I wrote this <Status> component. It renders null so it doesn't add anything to the page, but when it detects that it is rendering on the server, it uses its code prop to set staticContext.status.

All 4 comments

Some better documentation of this should be added. For the time being, you'll have to do with this comment:

When a <Route> matches, the element it creates is passed four props: history, match, location, and staticContext. staticContext is the object that you passed to <StaticRouter>. This means that you can modify your <NotFound> component like so:

class NotFound extends React.Component {
  componentWillMount() {
    const { staticContext } = this.props;
    if (staticContext) {
      staticContext.is404 = true;
    }

  render() { ... }
  }
}

Alternatively, you can "render" a status. For example, I wrote this <Status> component. It renders null so it doesn't add anything to the page, but when it detects that it is rendering on the server, it uses its code prop to set staticContext.status.

Hello @pshrmn,
thanks for the very clear explanation about how the staticContext works and how I can handle custom data in there.

I honestly like this approach as it seems very flexible (thus your nice approach in abstracting the status code through a dedicated component).

This should be added to the documentation soon as I believe it's an important missing point in React Router 4

Oh, I just noticed server rendering guide, which mostly covers this. That should probably also be added to the core documentation guides.

I'll still update the <StaticRouter context> documentation to be a little bit more obvious, but I think that it is currently just a visibility issue.

@pshrmn How would I manage to only have this NotFound component if I am rendering on the server side ?
Specifically, I have

    app.get('*', (req, res, next) => {
        const context = {};
        const htmlData = renderToString(
            <StaticRouter
                location={req.url}
                context={context}
            >
                <App/>
            </StaticRouter>
        );

        if (context.url) {
            winston.info(`Passing ${req.url} along`);
            next();
        } else {
            res.send(pageContent.replace('<div id="main"></div>',
                `<div id="main">${htmlData}</div>`));
        }
    });

I want to call next() if the router didn't match anything. I don't want to alter my client side code. Can I throw the NotFound component right below <App/> in the above code?

Thanks

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jzimmek picture jzimmek  路  3Comments

imWildCat picture imWildCat  路  3Comments

sarbbottam picture sarbbottam  路  3Comments

Waquo picture Waquo  路  3Comments

andrewpillar picture andrewpillar  路  3Comments