18

How can react-router properly handle 404 pages for dynamic content in a Universal app?

Let's say I want to display a user page with a route like '/user/:userId'. I would have a config like this:

<Route path="/">
    <Route path="user/:userId" component={UserPage} />
    <Route path="*" component={NotFound}  status={404} />
</Route>

If I request /user/valid-user-id, I get the user page.

If I request /foo, I get a proper 404.

But what if I request /user/invalid-user-id. When fetching the data for the user, I will realize that this user does not exist. So, the correct thing to do seams to be:

  • Display the 404 page
  • Return a 404 http code (for server side rendering)
  • Keep the url as is (I don't want a redirect)

How do I do that?? It seams like a very standard behaviour. I'm surprised not to find any example...

Edit:
Seams like I'm not the only one to struggle with it. Something like this would help a lot: https://github.com/ReactTraining/react-router/pull/3098
As my app won't go live any time soon, I decided to wait to see what the next react-router version has to offer...

Tom Esterez
  • 21,567
  • 8
  • 39
  • 44
  • 1
    How do you fetch the user data? I think the code there will be an essential part of the answer you are looking for. – Kees van Lierop Jan 21 '17 at 00:09
  • @KeesvanLierop I'm using redux. So I would have an thunk action returning a Promise. The error returned by the promise would indicate that the given userId does not exist. By default this action would be triggered by my UserPage component. But maybe it makes more sense to do it before actually rendering the component. I'm not sure... But I'm open to suggestions to make it work with react-router... – Tom Esterez Jan 21 '17 at 02:41

5 Answers5

1

First of create a middleware function for the onEnter callback, so that this is workable for redux promises:

import { Router, Route, browserHistory, createRoutes } from "react-router";

function mixStoreToRoutes(routes) {
    return routes && routes.map(route => ({
        ...route,
        childRoutes: mixStoreToRoutes(route.childRoutes),
        onEnter: route.onEnter && function (props, replaceState, cb) {
            route.onEnter(store.dispatch, props, replaceState)
                .then(() => {
                    cb(null)
                })
                .catch(cb)
        }
    }));
}

const rawRoutes = <Route path="/">
    <Route path="user/:userId" component={UserPage} onEnter={userResolve.fetchUser} />
    <Route path="*" component={NotFound}  status={404} />
</Route>

Now in this onEnter function you can work directly with the redux store. So you could dispatch an action that either successes or fails. Example:

function fetch(options) {
    return (dispatch) => {
        return new Promise((resolve, reject) => {
            axios.get('<backend-url>')
                .then(res => {
                    resolve(dispatch({type: `CLIENT_GET_SUCCESS`, payload: res.data}))
                })
                .catch(error => {
                    reject(dispatch({type: `CLIENT_GET_FAILED`, payload: error}));
                })
            }
        })
    }
}

let userResolve = {

    fetchUser: (dispatch, props, replace) => {
        return new Promise((next, reject) => {
            dispatch(fetch({
                user: props.params.user
            }))
                .then((data) => {
                    next()
                })
                .catch((error) => {
                    next()
                })
        })
    }

}

Whenever the resolve promise now fails, react-router will automatically look for the next component that it could render for this endpoint, which in this case is the 404 component.

So you then wouldn't have to use replaceWith and your URL keeps retained.

Kees van Lierop
  • 973
  • 6
  • 20
0

If you are not using server side rendering, returning 404 before the page gets rendered would not be possible. You will need to check for the existence of the user somewhere either way (on the server or via AJAX on the client). The first would not be possible without server side rendering.

One viable approach would be to show the 404 page on error of the Promise.

Yangshun Tay
  • 49,270
  • 33
  • 114
  • 141
  • Actually I'm ok with rendering the page for my 404. It's just the component associated with the route that shouldn't be rendered. I could use the Route's `onEnter` callback, do the request here and then call `replaceWith` if the user does not exist. But it don't realy like it because it would redirect the user to another url. BTW I'm in fact doing server side rendering so I'm looking for a universal solution. – Tom Esterez Jan 21 '17 at 06:57
0

I tried my solution in a project that I am making which uses Server Side Rendering and react-router and it works there, So I'll tell you what I did.

Create a function in which you'll validate an ID. If the ID is valid, Then return the with User page with proper Component, If the ID is invalid then return the with 404 Page.

See the example:

// Routes.jsx

function ValidateID(ID) {
    if(ID === GOOD_ID) {
        return (
            <Route path="/">
                <Route path="user/:userId" component={UserPage} />
                <Route path="*" component={NotFound}  status={404} />
            </Route>
        );
    } else {
       return (
           <Route path="/">
               <Route path="user/:userId" status={404} component={Page404} />
              <Route path="*" component={NotFound}  status={404} />
           </Route>
       );
    }

// Router.jsx

<Router route={ValidateID(ID)} history={browserHistory}></Router>

This should work with Server Side rendering as it did in my project. It does not uses Redux.

Ishan Jain
  • 665
  • 6
  • 20
  • 1
    Thanks, but could this strategy handle async check of the ID? – Tom Esterez Jan 27 '17 at 16:37
  • @TomEsterez I don't know, I didn't tried that, But `route` is getting value returned from ValidateID, So It probably doesn't matters if ID is being synchronously or asynchronously. – Ishan Jain Jan 27 '17 at 19:20
0

In case of dynamic paths, you can do it like this and you don't have to change the current path.

just import the error404 component and define a property(notfound) in the state to use for conditioning.

import React, { Component } from 'react';
import axios from 'axios';
import Error404 from './Error404';

export default class Details extends Component {
    constructor(props) {
        super(props)
        this.state = {
            project: {}, notfound: false
        }
    }

    componentDidMount() {
        this.fetchDetails()
    }

    fetchDetails = () => {
        let component = this;
        let apiurl = `/restapi/projects/${this.props.match.params.id}`;
        axios.get(apiurl).then(function (response) {
            component.setState({ project: response.data })
        }).catch(function (error) {
            component.setState({ notfound: true })
        })
    }

    render() {
        let project = this.state.project;
        return (
            this.state.notfound ? <Error404 /> : (
                <div>
                    {project.title}
                </div>
            )
        )
    }
}
Al Mahdi
  • 635
  • 1
  • 9
  • 16
  • How can this be set up so that every page doesn't need to have that ternary at the beginning of returning the component? – Adventure-Knorrig Jun 30 '22 at 12:07
  • You can do it for static paths like `/about` or `/contact`. But for dynamic paths like `/projects/100`, you have to load the component and send request to the server to check if the project exists in the db then you can render the project page or 404. – Al Mahdi Jun 30 '22 at 12:37
  • Ishan's answer(https://stackoverflow.com/a/41778478/13931061) might help you. – Al Mahdi Jun 30 '22 at 12:39
  • Ishan's answer is interesting, thanks for sharing – Adventure-Knorrig Jul 02 '22 at 09:56
0

I encountered a similar problem while making a blog website. I've been searching for a solution for a while now. I was mapping (using map function) my blog component based on dynamic link. The initial code snippet was as follows:

import Blog from '../../Components/Blog/Blog.component';

import './BlogPage.styles.scss';

const BlogPage = ({ BlogData, match }) => {

    return (
        <div className='blog-page'>
            {
                BlogData.map((item, idx)=>
                    item.link === match.params.postId?
                    <Blog 
                        key={idx} 
                        title={item.title} 
                        date={item.date} 
                        image={item.image} 
                        content={item.content}
                        match={match}
                     />
                    :''
                )
            }
        </div>
    )
};

export default BlogPage;

I used a hack where I would use filter function instead of map and store it and then check if it exists (in this case check if length greater than zero for result) and if it does the blog component is rendered with the props for the page else I render the Not Found component (My404Component). The snippet as follows:

import Blog from '../../Components/Blog/Blog.component';
import My404Component from '../../Components/My404C0mponent/My404Component.component';

import './BlogPage.styles.scss';

const BlogPage = ({ BlogData, match }) => {
    const result = BlogData.filter(item => item.link === match.params.postId);

    console.log(result);

    return (
        <div className={result.length>0? 'blog-page': ''}>
            {
                result.length>0?
                <Blog 
                    title={result[0].title} 
                    date={result[0].date} 
                    image={result[0].image} 
                    content={result[0].content} 
                    match={match} 
                 />
                :<My404Component />
            }
        </div>
    )
};

export default BlogPage;

This way the Blog component is not rendered as long as the value of the entered link is not valid as result would be an empty array and it's length would be 0 and instead My404Component would be rendered. The code is a little raw I havn't refactored it yet. Hope this helps.