React JS
React Learning Module

Routing with React

References:

There are a few different routing solutions for React but we’re going to focus on the most popular one: react-router.

Let’s install it:

npm install --save react-router react-router-bootstrap

To make sure our dev build supports the History API used by react-router and correctly references our files, we have to change and add the following to our webpack.config.js file:

...
  output: {
    path: BUILD_DIR,
    filename: '/scripts/bundle.js',
    sourceMapFilename: '[file].map'
  },
...
    new ExtractTextPlugin("/styles/style.css", {allChunks: false})
  ],
  devServer: {
    historyApiFallback: true
  }
...

Routing basics

Let’s create a Home.jsx component so we have at least 2 routes:

import React from 'react';

import { Jumbotron, Button } from 'react-bootstrap';
import { Link } from 'react-router';

class Home extends React.Component {
  render() {
    return (
      <div>
        <Jumbotron>
          <h1>Welcome to Our Awesome Store</h1>
          <p>
            We sell all kinds of drugs, kindly generated by <strong>Mockaroo</strong>.
            <br />
            Don't believe us?
            <br />
            <br />
            <Link to="/shop">
              <Button
                bsSize="large"
                bsStyle="primary">
                Head to the Store
              </Button>
            </Link>
          </p>
        </Jumbotron>
      </div>
    );
  }
}

export default Home;

In order to handle these routes, we have to make some changes to our index.js and App.jsx files.

The App.jsx file becomes:

import React from 'react';

import Header from './Header.jsx';
import Footer from './Footer.jsx';
// import ProductList from '../components/ProductList.jsx';
// import ProductPage from '../components/ProductPage.jsx';

import products from '../Products.json';

import { Grid } from 'react-bootstrap';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      products
    };
  }

  render() {
    return (
      <div>
        <Header />
        <Grid id="content">
          {
            React.cloneElement(
              this.props.children,
              {
                products: this.state.products
              }
            )
          }
        </Grid>
        <Footer />
      </div>
    );
  }
}

App.propTypes = {
  children: React.PropTypes.element.isRequired
};

export default App;

As App acts as our layout component, we are replacing the hardcoded ProductList component with a clone of the App Component’s children, also passing the products prop to them (if we wouldn’t need any extra props, we could have just placed the children there, without cloning them).

Now in index.js, instead of the App Component itself, we render a Router instance:

import React from 'react';
import { render } from 'react-dom';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';

import App from './components/App.jsx';
import Home from './components/Home.jsx';
import ProductList from './components/ProductList.jsx';

render(
  (
    <Router
      history={browserHistory}>
      <Route
        path="/"
        component={App}>

        <IndexRoute
          component={Home} />

        <Route
          path="shop"
          component={ProductList} />

      </Route>
    </Router>
  ), document.getElementById('app'));

Now let’s examine it: 1. The root route renders the App component 2. There are 2 child routes to it: - The IndexRoute which renders the Home Component; IndexRoutes resolve to the root of their parent routes - The shop Route, which resolves to /shop

Now if we go to http://localhost:8080/shop, we can see our ProductList, and if we go to http://localhost:8080 we can see that Home Component.

Navigating with react-router at its full potential takes a little more than regular anchors. You CAN use regular anchors, but you lose the SPA experience (the page will reload as it does with regular server-side-rendered websites).

To handle navigation with react-router, you have to use it’s Link components (which basically render to <a> tags) or handle navigation programatically. In our case, we also use LinkContainer components from react-router-bootstrap which are basically react-router wrappers for react-bootstrap components.

Let’s start with the Header:

import { IndexLinkContainer } from 'react-router-bootstrap';
...
        <Nav pullRight>
            <IndexLinkContainer to="/">
              <NavItem
                eventKey={1}>
                Home
              </NavItem>
            </IndexLinkContainer>

            <IndexLinkContainer to="/shop">
              <NavItem
                eventKey={2}>
                Shop
              </NavItem>
            </IndexLinkContainer>

            <NavItem
              eventKey={3}>
              <Glyphicon glyph="shopping-cart" />
              {' Cart'}
            </NavItem>
        </Nav>
...

Note: In order to set the current link active only if the active rute matches the one specified (but not its children), we are using IndexLink(Container)s.

Now let’s set the Product Component links to shop/${product.id}:

Product.jsx:

...
import { Link } from 'react-router';
...
        <div className="product-img-wrapper">
          <Link to={`shop/${product.id}`}>
            <img
              alt={product.name}
              className="img-responsive product-img"
              src={product.picture} />
          </Link>
        </div>

        <h4
          className="ellipsis"
          title={product.name}>
          <Link to={`shop/${product.id}`}>
            {product.name}
          </Link>
        </h4>
...

We are doing this because we want to display our product page on the shop/${product_id} route (if you click on the links now, you’ll get a blank screen and a warning that it doesn’t match any routes in the console).

To do so, we’ll first create a ProductPageWrapper component with the sole purpose of picking the right product to display in our ProductPage, according to the id from the URI. This can actually be achieved in a few different ways (for example doing this in the ProductPage Component’s constructor, by finding the desired product and setting it in the state), but we’ll do it this way:

ProductPageWrapper.jsx:

import React from 'react';

import ProductPage from './ProductPage.jsx';

class ProductPageWrapper extends React.Component {


  render() {
    const product = this.props.products.find((p) => {
      return p.id === this.props.params.id;
    });

    return product ? (
      <ProductPage
        product={product} />
    ) : (
      <div className="text-center">
        <h1>The product couldn't be found.</h1>
      </div>
    );
  }
}

ProductPageWrapper.propTypes = {
  products: React.PropTypes.arrayOf(React.PropTypes.object),
  params: React.PropTypes.object.isRequired
};

export default ProductPageWrapper;

react-router provides the route components with the params prop (and a few other), which is a deserialized representation of the URI parameters.

At last, in index.js, change the shop route:

import ProductPageWrapper from './components/ProductPageWrapper.jsx';
...
        <Route
          path="shop">

          <IndexRoute
            component={ProductList} />

          <Route
            path=":id"
            component={ProductPageWrapper} />

        </Route>

Now clicking on a product image / name in the Product list, will bring us to the respective Product Page.

If we go to a route that’s not defined, like http://localhost:8080/notdefined , we’ll get a blank page and an error in the console browser.js:49 Warning: [react-router] Location "/notdefined" did not match any routes.

It’d be better to show a 404 page. So let’s create one:

NotFound.jsx:

import React from 'react';

import { Image } from 'react-bootstrap';

function NotFound() {
  return (
    <div>
      <Image
        src="http://techfar.com/wp-content/uploads/2013/03/404-page2.png"
        alt="Page not found"
        className="img-responsive center-block" />
    </div>
  );
}

export default NotFound;

Now in index.js, inside the / route add a “match-all” route:

...
        <Route
          path="*"
          component={NotFound} />
...

Now if we go to any inexistent route, we get a nice 404 page.

Still, if we go to a route like http://localhost:8080/shop/badproductid we get errors about product being undefined and a blank screen, which is undesirable.

This can also be easily fixed by changing the ProductPageWrapper as follows:

import React from 'react';

import ProductPage from './ProductPage.jsx';
import NotFound from './NotFound.jsx';

class ProductPageWrapper extends React.Component {


  render() {
    const product = this.props.products.find((p) => {
      return p.id === this.props.params.id;
    });

    return product ? (
      <ProductPage
        product={product} />
    ) : (
      <NotFound />
    );
  }
}

ProductPageWrapper.propTypes = {
  products: React.PropTypes.arrayOf(React.PropTypes.object),
  params: React.PropTypes.object.isRequired
};

export default ProductPageWrapper;

It’s pretty simple: if no product matching the ID from the URI matches, it shows the 404 page instead of the Product page.

Of course, there is more to routing than just this, some points we’ll touch in the next steps, some I’ll leave you to discover and experiment with.