1. Learn

Context-free Routing

Learn how to create a simple push-state based React router that doesn't rely on the experimental context API.

James K Nelson

James is the editor of React Armory, and has been creating things with JavaScript for over 15 years.

Read more by JamesFollow @james_k_nelson on Twitter

Routing is the process of choosing what to render based on the browser’s current URL.

Not all apps need to perform routing. For example, single-screen utilities and games will often not need a router at all. But if your app does have links and URLs, handling URL changes effectively is important for UX and accessibility.

Existing routing solutions

React itself does not have a built-in routing solution. Because of this, there are a number of community projects that can handle it for you, including the popular react-router project.

These projects each have their benefits, but they also introduce dependencies. In particular, many rely on React Context – an experimental API that the React maintainers ask you to avoid.

Luckily, it is surprisingly simple to create a simple router that doesn’t rely on context at all. Component state and lifecycle are all you need to hook React up to the relevant browser APIs. This guide will take you through the process of building a simple context-free Router using these tools.

Switching content by URL

The simplest way to implement routing in a React application is with a top-level Component that chooses which content to render by URL. For example, a top-level TopScreen component may decide what to render based on window.location.pathname:

function Menu() {
  return (
    <div>
      <a href="/news">News</a>
      <a href="/profile">Profile</a>
    </div>
  );
}

function TopScreen() {
  const location = window.location;

  let content;
  if (location.pathname === '/news') {
    content = <h1>News</h1>;
  } else if (location.pathname === '/profile') {
    content = <h1>Profile</h1>;
  } else {
    content = <h1>Not Found</h1>;
  }
  
  return (
    <div>
      <Menu />
      <div>{content}</div>
    </div>
  );
}

But while this app will work, it has one major issue; it will reload the entire page every time a user clicks a link.

HTML5 History

The HTML5 History API makes it possible to update the browser’s URL without causing a page reload. It also emits browser events when the user clicks Back or Forward. By using this API, you can write applications that respond faster and behave as expected when Back or Forward are clicked.

Single page applications will often make use of the following two parts of the History API:

  • The popstate browser event on the window object, which is emitted when Back or Forward are clicked
  • The window.history.pushState() method, which allows the app to replace the contents of the URL bar without actually navigating

History objects

The HTML5 History API is not without issues.

Firstly, there are inconsistencies between how the different browsers use the popstate event. Some versions of Chrome and Safari will emit an event on page load while other versions won’t.

Secondly, navigation events are only emitted when they’re caused by external stimuli such as the Back or Forward buttons. When the application itself updates the URL, nothing will happen. This means that the application will need to manually pass the new URL from the link that caused it to the top-level component that is handling routing.

Typically, these issues are solved by creating a separate object which manages access to the HTML5 History API. This object will do two things:

  • It will normalize events between browsers
  • It will emit navigation events caused by both the application and external stimuli

While it is certainly possible to create your own object to manage access to the History API, in this guide we’ll use the popular history package.

The history package

Assuming your project is using webpack or create-react-app, start by adding the history package to your project:

npm install --save history

If you're not using webpack or create-react-app, you may want to reconsider whether your app even needs URL-based routes.

As the History API is global, our object will also need to be accessible everywhere. For example, we can create a separate file and export the object from it, so that any other modules can later import it.

import createBrowserHistory from 'history/createBrowserHistory';

const browserHistory = createBrowserHistory();

export default browserHistory;

This guide will use the following methods and properties from the browserHistory object:

  • browserHistory.location

    An object that contains the browser’s current location. Its shape is similar to the object available at window.location:

    {
      pathname: '/friends',
      query: '?page=1',
      hash: '#top'
    }
    
  • browserHistory.listen(callbackFunction)

    Subscribes to history events, returning a function that will cancel the subscription when called.

    While subscribed, the callback function will be called with the latest location each time the user navigates. For example:

    const unlisten = browserHistory.listen((location) => {
      console.log(location.pathname);
    });
    
    
  • browserHistory.push(location)

    Adds a new entry to the browser history using window.history.pushState(), then calls any listeners registered with browserHistory.listen()

You can view the documentation for the full browserHistory object API at the history package’s page.

By default, HTML <a> elements will always cause the page to reload when clicked. If you’d like to update the URL using the History API, you’ll need to cancel the default behavior and then implement an alternative.

You could do this by adding an onClick handler to each individual <a> element, but since most links within an app will share the same logic, React applications will often use a component to handle links. Instances of this component behave exactly like <a> elements, except that instead of refreshing the page, they will update the URL in-place using browserHistory.push():

This Link object assumes the browserHistory object is imported from the file defined earlier.

If you don’t want to rely on importing a global browserHistory object, you could also pass the object through React context. It is safe to use context in this situation as there will only ever be a single browserHistory object.

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

    this.handleClick = this.handleClick.bind(this);
  }

  handleClick(event) {
    const isLeftClick = event.button === 0;

    // The browser or OS will sometimes implement different behaviors when the
    // user holds a modifier key while clicking
    const isModified = event.metaKey || event.altKey || event.ctrlKey || event.shiftKey;

    // An `<a>` element's `target` prop lets you specify that the link should
    // be opened in a different window
    const hasNoTarget = !this.props.target;

    // We shouldn't replace the browser's default behavior in special cases
    if (isLeftClick && !isModified && hasNoTarget) {
      // Prevent the browser from navigating
      event.preventDefault();

      // Instead of a normal URL, browserHistory.push() expects an object with
      // `pathname` and `search` properties
      const pathname = this.props.href.split('?')[0];
      const search = this.props.href.slice(pathname.length);
      const location = {pathname, search};

      // Update the browser URL and notify any listeners added
      // via browserHistory.listen()
      browserHistory.push(location);
    }
  }

  render() {
    return (
      <a {...this.props} onClick={this.handleClick}>
        {this.props.children}
      </a>
    );
  }
}

Use <Link> wherever you would normally use <a>. For example, the Menu component from the first example would use <Link>:

function Menu() {
  return (
    <div>
      <Link href="/news">News</Link>
      <Link href="/profile">Profile</Link>
    </div>
  );
}

Handling history callbacks

When the URL is updated by browserHistory.push(), nothing will happen by default. In order to update the displayed content, you will need to listen for history events and take appropriate action.

Since your TopScreen component decides what content to display, the current location will need to be available within that component’s render() method. You can accomplish this by storing the current location directly on state, using the following process:

I’ve assumed that you’ll store the location property of this.state. Of course, you can call it whatever you’d like.

  1. Set the initial state to the current value of browserHistory.location.
  2. Subscribe to history events once the component has initially rendered.
  3. Update the state whenever a new location is emitted, causing a re-render.
  4. Unsubscribe from history events when the component is unmounted.
class TopScreen extends React.Component {
  constructor(props) {
    super(props);
    
    // Set the initial location
    this.state = {
      location: browserHistory.location
    };
  }
  
  componentDidMount() {
    // Listen for navigation events only after the component is mounted,
    // and save the latest location to component state.
    //
    // We do this here instead of in `constructor()`, as listening for
    // navigation events only makes sense when the component is mounted in
    // a browser -- not when it is being rendered server-side.
    this.unsubscribe = browserHistory.listen(location => {
      this.setState({location});
    });
  }
  
  componentWillUnmount() {
    // Ensure our listener does not keep calling `setState` after
    // the component has been unmounted
    this.unsubscribe();
  }

  render() {
    const location = this.state.location;
  
    let content;
    if (location.pathname === '/news') {
      content = <h1>News</h1>;
    } else if (location.pathname === '/profile') {
      content = <h1>Profile</h1>;
    } else {
      content = <h1>Not Found</h1>;
    }
    
    return (
      <div>
        <Menu />
        <div>{content}</div>
      </div>
    );
  }
}

The Router component

If you listen for location changes directly within the component that uses them, the component will be tied to the browser, preventing you from re-using it in a server-side environment.

A more flexible approach is to manage the current location within a separate App component, passing its value to the view component via props.

class App extends React.Component {
  constructor(props) {
    super(props);
    
    // Set the initial location
    this.state = {
      location: browserHistory.location
    };
  }
  
  componentDidMount() {
    // Listen for navigation events only after the component is mounted,
    // and save the latest location to component state.
    //
    // We do this here instead of in `constructor()`, as listening for
    // navigation events only makes sense when the component is mounted in
    // a browser -- not when it is being rendered server-side.
    this.unsubscribe = browserHistory.listen(location => {
      this.setState({location});
    });
  }
  
  componentWillUnmount() {
    // Ensure our listener does not keep calling `setState` after
    // the component has been unmounted
    this.unsubscribe();
  }

  render() {
    return <TopScreen location={this.state.location} />;
  }
}

By passing location via props, your TopScreen component can be implemented as a simple function.

function TopScreen({ location }) {
  let content = <Menu />
  if (location.pathname === '/news') {
    content = <h1>News</h1>;
  }
  else if (location.pathname === '/profile') {
    content = <h1>Profile</h1>;
  }
  else {
    content = <h1>Not Found</h1>;
  }

  return (
    <div>
      <Menu />
      <div>{content}</div>
    </div>
  );
}

Nesting routes

Handling all of your routes within a single TopScreen component works to a certain point. But as your app grows, the amount of conditional logic required will become unwieldy.

One solution to this is to match only part of a route, passing the remainder of the route onto a subsequent screen – and this will be covered in the next update to this guide, which will published mid-August. Join React Armory to make sure you don’t miss the update, which will add live examples and cover:

  • parameter matching
  • 404 handling
  • adding links to composable components

Join React Armory,
Get Cool Stuff

Exclusive access to resources.

Early access to new content.

No spam, ever.

React Armory is growing. Join in to be the first to experience new lessons, examples and resources.