React-router: how to render 404 page without redirect properly?

Created on 9 Oct 2015  路  8Comments  路  Source: ReactTraining/react-router

I've setup a dynamic route right at the root and validate the url via a custom function like this:

function verifyUrl(state, replaceWith) {
  if (state.params.user.search(/(david|goliath)/) === -1) {
    state.location.error = true // UGH, UGLY!
  }
}

const routes = (
  <Route path='/' component={require('./views/Handler')}>
      <Route
        onEnter={verifyUrl}
        path=':user'
        component={require('./views/User')}
        />
    <Route path='*' component={require('./views/NotFound')}/>
  </Route>
)

Now, I don't like to redirect to some /404 page with replaceWith but instead, I'd like to render the NotFound page immediately.

Question is, how to I switch out the component= now?

On the server, my code looks like this right now (notice I just send 'Not Found' atm)

match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
  if (error) {
    res.status(500).send(error.message)
  } else if (redirectLocation) {
    res.status(302).redirect(redirectLocation.pathname + redirectLocation.search)
  } else if (renderProps) {
    if (renderProps.location.error) { // better way of doing this?
      // I'd like to render the NotFound component here instead now.
      res.status(404).send('Not Found')
    } else {
      const content = renderToString(createElement(RoutingContext, renderProps))
      const page = IndexPage.renderToStaticMarkup({ content })
      res.status(200).send(page)
    }
  } else {
    res.status(404).send('Not Found')
  }
})

Most helpful comment

Here is another use case. Generally REST APIs can return 404 HTTP error messages for wrong routeParams.

Take this example:

We have a resource that lives - once it was created - under a hash.

POST /resources/

201 Created 
Location: /resources/17SPPaafEfAFj7Xe9Mr8oVh9vSQrQJcYF/

When a user is now pointed to the same route /resources/ in the browser/frontend, the frontend will first need to fetch the data from the REST API, which then - if the user mistyped the URL - will return a 404 HTTP response that needs to be propagated to the user somehow.
In that case the data was fetched asynchronously and inside this pages wrapper/outer component.

Generally we don't want to do a redirect using this.history.pushState/replaceState, as we would lose the user's context at that point (we shouldn't redirect from /resources/<hash>/ to /404/).
What we want is to stay on the same domain and display a generic error component that can take the errors input to help the user navigate to a more valid state again.

Even though it sounds totally crazy, I'd be more in favor of letting the component itself throw some kind of interception/exception to signal the high level history controller that something has gone wrong, for it to act accordingly.

All 8 comments

It would be great if onEnter hook had additional next() method similar to Express.

Calling it without parameters would continue url matching agains other routes, while passing an error would stop matching with an error.

It this case it will be very simple to validate route params and call next() if they are invalid, falling through route config till the 404 handler.

It will also allow to use routes with overlapping paths (which is impossible now), for example /:userLogin and /:postId.
One could asynchronously check for userLogin existence and if it's not there call next() to fall to post route handler.

I think it would be awesome feature! Moreover, this API is already well-known to all Express users.

@mjackson What do you think?

FWIW, I don't think that's an optimal pattern for dynamic route matching. Something like Werkzeug's custom rules/converters seems much cleaner and more declarative to me: http://werkzeug.pocoo.org/docs/0.10/routing/#rule-format. Though consistency with Express is a pretty strong point.

Well, if you happen to find a javascript library, lemme know @taion. But for the stuff I do, a simple regex is fine.

Btw, looks like https://github.com/rackt/react-router/blob/master/docs/API.md#getcomponentlocation-callback is what I need?

You could use something like this:

function getUserComponent(state, replaceState) {
  if (state.params.user.search(/(david|goliath)/) === -1)
    return require('./views/NotFound')

  return require('./views/User')
}

const routes = (
  <Route path='/' component={require('./views/Handler')}>
    <Route path=':user' getComponent={getUserComponent} />
  </Route>
)

@mjackson What do you think about the next() thing I described above? If it makes sense to you I could try to implement it.

I'm not speaking for anyone but myself here, but I really think it'd be cleaner for all of the matching to take place while actually doing matching, and not bounce through onEnter. Something like:

<Route rule={rule`<${createRegexRule(/foo/)}:name>`} />
<Route rule={rule`<${intRule}:number>`} />
<Route rule={rule`<${uuidRule}:id>`} />

or whatever.

Here is another use case. Generally REST APIs can return 404 HTTP error messages for wrong routeParams.

Take this example:

We have a resource that lives - once it was created - under a hash.

POST /resources/

201 Created 
Location: /resources/17SPPaafEfAFj7Xe9Mr8oVh9vSQrQJcYF/

When a user is now pointed to the same route /resources/ in the browser/frontend, the frontend will first need to fetch the data from the REST API, which then - if the user mistyped the URL - will return a 404 HTTP response that needs to be propagated to the user somehow.
In that case the data was fetched asynchronously and inside this pages wrapper/outer component.

Generally we don't want to do a redirect using this.history.pushState/replaceState, as we would lose the user's context at that point (we shouldn't redirect from /resources/<hash>/ to /404/).
What we want is to stay on the same domain and display a generic error component that can take the errors input to help the user navigate to a more valid state again.

Even though it sounds totally crazy, I'd be more in favor of letting the component itself throw some kind of interception/exception to signal the high level history controller that something has gone wrong, for it to act accordingly.

For redirecting upon receiving 404 api responses, I found using history.state to be useful...

First, I added the following the top of my <Switch> statement

<Switch>
    {location.state && location.state.notFound && <Route component={NotFound} />}
    // other routes
</Switch

Then in the api call...

fetch('/things/1234')
  .catch((e) => {
    if (e.response.status === 404) {
      history.replace({
        state: {
          notFound: true
        }
      });
    }
  })
Was this page helpful?
0 / 5 - 0 ratings

Related issues

alexyaseen picture alexyaseen  路  3Comments

hgezim picture hgezim  路  3Comments

ackvf picture ackvf  路  3Comments

yormi picture yormi  路  3Comments

Radivarig picture Radivarig  路  3Comments