Building a Router with Raw React

So you’ve decided to build a Single Page App with React, and everything seems to be going dandy. You’ve got yourself some wireframes, a HTML file and a few components, and then you decide to add some routes. Easy, right?

Well, thats what you thought until you started reading the internet. But now you’re worrying about isomorphism and the HTML 5 history API and even how to pass props to your view components again. And if you thought learning all this was painful, imagine rewriting your application when the routing library’s API breaks in a few weeks.

Routing doesn’t have to be complicated, so why stress yourself out with libraries when a hand-rolled router can take less than 20 lines? Especially seeing that if you’d have just kept following this guide, you would have had something working in only two minutes

Hash-based routing in two minutes

Routing means doing something in response to a change in the browser’s current URL. There are two ways you can accomplish this:

  1. pushState routing, using the HTML5 History API
  2. hash-based routing, using the portion of the page’s URL starting with #, i.e. the hash.

Hash-based routing is by far the simpler of the two alternatives, and with the exception of a few specific cases, it’ll usually do the job. So let’s go with this.

Implementing hash-based routing with React is simple; just choose what to render based on the string stored in window.location.hash. We’ll do this once on page load, and again each time the browser emits the hashchange event:

// Handle the initial route
navigated()

// Handle browser navigation events
window.addEventListener('hashchange', navigated, false);

Given the above two lines, all we need to do to finish our router is implement the navigated function. And since you won’t learn anything without putting it into practice, let’s do this as an exercise.

Exercise 1: Create a hash-based router

The specification for navigated is simple; it calls ReactDOM.render, with the passed in component depending on the value of window.location.hash.

Your task is to implement the navigated function, handling the following hashes:

  • For #/, use a component containing the text I'm amazing! I've made a Raw React Router!
  • Otherwise, use a component containing the text Not Found

If you need a HTML file to test your script with, use the file from part 1‘s Exercise 1.

Once you have tested your work by entering in various URLs, compare your solution to mine by touching or hovering your mouse over this box:

function navigated() {
  // Choose which component to render based on browser URL
  var component = window.location.hash == "#/"
    ? React.createElement('div', {}, "Index Page")
    : React.createElement('div', {}, "Not Found")

  // Render the new component to the page's #react-app element
  ReactDOM.render(
    component,
    document.getElementById('react-app')
  );
}

Congratulations, you now know how to build a working router! Given enough time, you could use what you’ve learned to build a full-featured routing system.

Of course, while this incredibly basic router works, trying to integrate it with our contact list app from parts one and two is not going to scale. So let’s learn how to apply these fundamentals to a real app.

This is part three of my series on Raw React. If you’re new to React.js, start from part one. Otherwise, you can get your bearings at part two’s GitHub repository.

Managing the current location

In the example above, we’ve directly referenced window.location.hash when choosing what to render:

var component = window.location.hash == "#/"
    ? React.createElement('div', {}, "Index Page")
    : React.createElement('div', {}, "Not Found")

As we learned in part one, React apps don’t re-render themselves. Because of this, when window.location.hash changes value, we need to manually call ReactDOM.render to update the DOM. We had no trouble doing this in the first exercise, but how would we go about applying this to our contact list application?

Review: The Story So Far

In our contact list application from parts one and two, ReactDOM.render is never called manually. Instead, it is called from within our setState function. But what does the setState function have to do with rendering?

The setState function – as you may expect – is used to update the current application state. This state is stored in a global state variable – but crucially – we make sure to never update this variable directly. And because all updates to state happen through the setState function, we also know that the only time the app must be re-rendered is within each call to setState. That is – as long as the application’s state is completely stored in state.

One more thing. As setState is a global, we could call it from anywhere. But we’ve decided to only call setState from functions which directly handle user input. We call these functions actions, and place them all in a single location within the source code, passing them via props to where they are required.

Location as state

But now that we’ve remembered how our app fits together, it seems we have a problem: our simple hash-based router requires that we call ReactDOM.render manually, but our app requires that ReactDOM.render is called from within setState.

Can you think of a way to reconcile this? Have a think about it, then check your intuition by touching or hovering your mouse over this box:

Instead of directly accessing window.location.hash each time we render, we can instead store it inside our state object by calling setState within the hash change handler. In other words, we can turn our navigated function into an action.

Exercise 2: Adding routing to your contact list

Lets continue from where we left off in part 2, and give our contact list app some routes! To start, we’ll keep it simple by providing only two routes:

  • #/contacts, which displays the existing contact list
  • a default route, which displays a “Not Found” message and a link to #/contacts

Your task is to implement the following changes:

  • Add a navigated function which stores the current hash in state.location
  • Call navigated on page load, and on subsequent hash change events
  • Modify setState to render the correct content for the current value of state.location

Once you’ve got it working (or gotten stuck), compare your answer with mine:

JS Bin on jsbin.com

Are the JSBin examples getting a little hard to follow? A GitHub repository with the source split over multiple files is available with this guide’s final solution.

Of course, a single page app shouldn’t literally be a single page. So let’s add an edit form!

Extracting route parameters

We’d like to be able to specify the contact we want to edit by adding its id to the hash, following this pattern:

#/contacts/<id>/

The <id> part of the above hash is called a route parameter. Note how the parameter is delineated by / characters; you might be familiar with this if you’re used a server side tool like Express or Ruby on Rails.

Actually, it isn’t just route parameters that we delineate with / – our route names are also sandwiched between slashes. Given this is the case, lets make our job easier by storing an array of parts in state.location, as opposed to the hash itself:

['contacts', '<id>']

Did you notice how I didn’t write ['#', 'contacts', '<id>'] or ['', 'contacts', '<id>', '']? While these are perfectly valid ways of storing your current route, the information we care about is all located between the first and final / characters. So let’s cut the crusts off:

// Removes the `#`, and any leading/final `/` characters
window.location.hash.replace(/^#\/?|\/$/g, '').split('/');

But James, you ask – won’t routing based on URLs get me into trouble in the long term? If my URL structure changes, refactoring it will be a nightmare! Actually, you’re spot on.

URL parsing tools

While this article is about the fundamentals of routing with React, for any real project you’ll probably want to use a tool to name your routes and route parameters, and to lookup/generate URIs using these names.

The tool I use for this purpose is called uniloc, and is part of Unicorn Standard – my collection of tools for JavaScript-based Single Page Applications. And on the odd chance you’d like to learn how to build a real react app, you can get a version of the contact list project extended to use uniloc just by signing up to hear about my latest articles and tools! But I digress.

Get the uniloc/Raw React example project

Exercise 3: Selecting component props by hash

Now that you know how to extract the route parameters, lets actually set up a (read-only) contact form. Heres the view component we’ll use:

var ContactView = React.createClass({
  propTypes: {
    contacts: React.PropTypes.array.isRequired,
    id: React.PropTypes.string.isRequired,
  },

  render: function() {
    var key = this.props.id;
    var contactForm = this.props.contacts.filter(function(contact) { return contact.key == key })[0];

    return (
      !contactForm
        ? React.createElement('h1', {}, "Not Found")
        : React.createElement('div', {className: 'ContactView'},
            React.createElement('h1', {className: 'ContactView-title'}, "Edit Contact"),
            React.createElement(ContactForm, {
              value: contactForm,
              onChange: function(){},
              onSubmit: function(){},
            })
          )
    )
  },
});
Take special note of this view component’s props: while the contact objects from part two specify a key value and no id, this view’s props specify an id but no key. This is because key is a special prop which is consumed by React. See the React documentation for more details.

Your task is to display the ContactView under the /contacts/<id>/ route, and add links to this view to the contact list.

Once you’ve got this working, compare your answer with mine:

JS Bin on jsbin.com


Your app is finally starting to take shape! You can add contacts, navigate between pages, and you can even use the browser forward/backward buttons (don’t laugh, a lot of web apps fall to pieces when the user touches them).

But while it’s great that you’ve managed to get this far, having a giant switch statement smack bang in the middle of setState obviously isn’t going to scale. So let’s fix this with an Application component.

The Application component

The Application component is the component we’ll pass to ReactDOM.render. It takes the state global as its props, and returns the rest of the application:

ReactDOM.render(
  React.createElement(Application, state),
  document.getElementById('react-app')
);

This seems pretty simple, but it’s actually a really big deal. Why?

One of React’s biggest strengths is that the stateless components typically used with it encourage you to design apps which are easy to reason about. By specifying your entire user interface’s state with a single state object, you cleanly separate your app into two parts:

  • A model, which manages your application state
  • A view, which defines how to render that state

Your Application component is the interface between model and view. Because of this, a well written Application component will at a glance show you how your entire app fits together.

Actions, Callbacks & Application

As part of wiring your application together, your Application component is also responsible for passing the correct actions to the views which it renders:

React.createElement(ContactsView, Object.assign({}, this.props, {
  onChangeContact: updateNewContact,
  onSubmitContact: submitNewContact,
}));
New to Object.assign? Read about it at Mozilla Developer Network.

But as our app currently stands, all actions are global functions. So why not just use these action functions directly from the view components?

When asking this question, it helps to remember the reason we’re using React in the first place – we want to be able to compose our application from reusable components. Using global actions within view components ties them to the specific application, while at the same time hiding the component’s dependencies from anyone trying to grok the app through its Application component.

Exercise 4: Implement your Application component

Implementing your Application component is simple: all you need to do is shift the functionality which is currently inside your setState function to a new Application component.

Also, since we’ll eventually want to handle user input in our edit form, let’s take this exercise as an opportunity to add empty updateContactForm and submitContactForm actions. These actions should be passed from Application into the ContactView component, which will in turn pass them to ContactForm.

Your task is to implement the Application component, with empty updateContactForm and submitContactForm handlers.

When you’ve finished, compare your implementation with mine. It should be nearly identical:

JS Bin on jsbin.com


And with that simple change, your application’s guts are now all clean and tidy! But your users won’t care if the “Save” button doesn’t work. So let’s fix it.

Navigating programatically

Action functions like the one called by “Save” often need to send the user to a different page. But while <a href="#/..."> tags provide a simple way to let users navigate, they won’t help us when we want to change the location programatically. So what are we to do?

Our first thought might be to simply change our “Save” button to an <a> tag styled like a button. But this will not allow us to call an action function; the user will be able to navigate, but won’t be able to save the form’s contents. So instead, let’s mimic the functionality of <a> from within our action by using the window.location.replace function:

// Navigate to `#/contacts`
window.location.replace(
  window.location.pathname + window.location.search + '#/contacts'
);

Exercise 5: Navigate within an action

When the user clicks on the “Save” button before the form input has changed, the user will still expect to be taken back to the contacts list.

Your task is to implement the submitContactForm action such that it sends the user to the contact list.

When you’ve finished, compare your implementation with mine by touching or hovering over this box. It should be nearly identical:

function submitContactForm() {
  window.location.replace(
    window.location.pathname + window.location.search + '#/contacts'
  );
}


Wonderful, the user will no longer be confused when they press the save button and nothing happens! But, given the app immediately displays “Not Found” when it is loaded, there’s a good chance they’ll never even see the save button. Let’s do something about that.

Redirecting

Redirecting is just a fancy way of saying “sending the user to location A when they request location B”. And now that we know how to navigate programatically, implementing redirection is simple: just update window.location as we’re handling navigation events:

function navigated() {
  var normalizedHash = window.location.hash.replace(/^#\/?|\/$/g, '');

  if (normalizedHash == 'some-route') {
    window.location.replace(
      window.location.pathname + window.location.search + '#/another-route'
    );
  }
  else {
    setState({
      location: normalizedHash.split('/')
    });
  }
}

The only trick here is to make sure you don’t update setState until you’ve reached the final destination. This is because updating window.location will cause navigated to be called once the browser has updated its URL. If you update setState on each step you take, you’ll end up rendering each intermediate route. This will cause performance issues; more importantly though, it will look terrible.

Why redirects are necessary

Within a single page app (or the web in general), you generally want to ensure that what is visible in the address bar:

Address bar

Corresponds to what is actually visible within the page:

Page content

James, you say – I already know that. I’ve just read through an entire article on routing for crying out loud – and besides – this still doesn’t explain why redirects are necessary. But the reason I brought it up is that sometimes you have a URL which corresponds to more than one page.

The textbook example of this is the root location, i.e. #/. Do you know why the content underneath #/ may vary? Have a quick think about it, then check your answer by touching or hovering your mouse over this box:


The root location is the first location a user will see when they open an app. However, what a user wants to see initially will depend on what they’ve seen before.

Is the user a logged-in customer? They’ll probably want to see their contacts. Are they someone completely new? We’ll want to show them the registration form.

In order to ensure that the app’s URL and content match, you’ll need to redirect the user from the root location to the hash which matches their desired view.

Exercise 6: Handle the root location

Now that you know why you need a root redirect and how to implement it, why not give it a shot?

And while you’re at it, since the app now navigates programatically in multiple places, refactor your code by replacing your existing call to window.location.replace with a new startNavigating(newLocation) action.

Your task is to implement a redirect from the root location to #/contacts, using your new startNavigating function.

Once you’ve finished, compare your implementation with mine by touching or hovering over this box. It should be nearly identical:

function submitContactForm() {
  startNavigating('/contacts');
}

function navigated() {
  // Strip leading and trailing '/'
  normalizedHash = window.location.hash.replace(/^#/?|/$/g, '');

  if (normalizedHash === '') {
    // Redirect for default route
    startNavigating('/contacts');
  }
  else {
    // Otherwise update our application state
    setState({location: normalizedHash.split('/')});
  }
}

function startNavigating(hash) {
  window.location.replace(
    window.location.pathname + window.location.search + '#' + hash
  );
}


Great work! At this point, there is only one thing left: making the edit form work. And, given you’ve completed part 2, you should already be capable of completing this yourself! But before you do, there is one more thing you should know about:

Transitioning between locations

If we were to draw a timeline of the transition between two routes, it would look something like this:

Flow with one render

When the current location changes primarily because the user wants to see something else, this makes total sense. The user’s intent is to navigate, so the only property under state which needs to change is location.

But what happens when navigation happens for some other reason, like submitting changes to a contact? Let’s have a look! Assuming we implement our submitContactForm action with something like the following:

startNavigating('#/contacts');
setState({contacts: updateContacts});

Our flow will look like this:

Flow with two renders

There is a problem here. Do you see it? Once you think you do, check your understanding by touching or hovering your mouse over this box:


ReactDOM.render first renders the new view component without the updated contacts, then re-renders it with the updated data. This slows things down, but more importantly will also result in old data briefly flashing on screen.

To eliminate this problem, we can add a new transitioning property to our state object. When navigation starts, state.transitioning will be set to true, and when complete, it will change back to false. By ensuring setState only renders when state.transitioning is false, we eliminate the double render.

If we were to handle state.transitioning in our Application component’s render method instead, it would also be possible to apply CSS transitions between views.

Exercise 7: Complete implementation of the edit form

With the contact list app being almost complete, there is only one thing left to do:

Your task is to make the edit form work.

This will involve:

  • Adding a new object to state which stores the current values of the various contact’s edit forms
  • Updating your ContactView component to show the edited data
  • Completing the updateContactForm and submitContactForm actions

Be careful to make sure that the entered data is not saved until the user actually presses “Save”. Also, ensure that validation is performed correctly and any errors are displayed properly.

Finally, don’t forget to implement state.transitioning! While it may not feel necessary with an app of this size, ensuring that ReactDOM.render is not called multiple times will become increasingly important as your app starts to grow.

Once you’re happy with your implementation, compare it to this guide’s GitHub repository. Make sure your features match the specification, but don’t get too hung up on differences in implementation.


And there you have it, you now know the fundamentals of routing with React! Pat yourself on the back for a job well done!

Your next steps

Now that you know how to build a router, the next step is to go out and do it, right?

Hold on a minute. The next step actually depends on what you’re building. Are you creating a tiny app with only three or four routes? In that case, its time to get started! But what if you’re building something a little bigger?

In order to avoid 1000-line Application components it will become increasingly important to focus on how your routes are defined as your app starts to scale. And while nothing is stopping you from implementing this yourself, not everything is best solved with Raw React.

Don’t reinvent the wheel

The last thing I want anyone to take away from this guide is that tools are bad. Tools are incredibly important. But — if all you have is a hammer, everything looks like a nail. Without the knowledge of how a router works, all you really have is react-router et al.

Now don’t get me wrong – react-router is a great tool for a number of use cases. But now that you know how routing works, you should be able to pick the best tool for your use case. And on the odd chance that you decide this is a Raw React, hash-based router, you’ll probably want a tool to help map URLs to routes. And as it happens, I made uniloc just for this purpose. And to give you a head start, I’ve put together a small example project for my subscribers which marries uniloc with your contact list app.

In return for your e-mail address, I’ll send you the next episodes in the series as they’re released. As a bonus you’ll also immediately receive the uniloc-enhanced project source, and 3 print-optimised PDF cheatsheets – on React, ES6 and JavaScript promises.

I will send you useful articles, cheatsheets and code.

I won't send you useless inbox filler. No spam, ever.
Unsubscribe at any time.

One more thing – I love hearing from readers. If you have something to say, send @james_k_nelson a tweet, or send me an e-mail at james@jamesknelson.com. Thanks for reading!


Raw React Routing


Read More

Related Links

Share on Facebook0Tweet about this on TwitterShare on LinkedIn2Share on Reddit18

3 Comments Building a Router with Raw React

  1. azi

    contribute:

    // with #/product/id
    var normalizedHash = window.location
    .hash.slice(2);
    // reproduce product/id
    window.location
    .hash.slice(1);
    // /product/id


    the state:

    window.location
    .hash.slice(1).split('/')
    // out: ["","product","id"]
    window.location
    .hash.slice(1).split('/').splice(1,2);
    // out: ["product",'id']

    the next issue: approach routing more “expressive”,
    and use RegEXP `*, + ? /:`?

    `/product/:id`
    `/product/category/:id*`
    `/product/:number(\\d+)`
    `/product/?search`

    test case:

    '/product/:id'.match(/^\/product\/([^\/]+?)\/?$/i)
    // out: path:'product', catch:':id'

    trying1:
    route: ‘/product/:120’
    path: /product/id

    RegEXP: /^\/product\/([^\\/]+?)(?:\/(?=$))?$/i
    Keys: 120,
    path:id=120

    Reply
  2. Marcos Rodrigues

    I’ve found the final implementation for submitContactForm extremely complex. One of the things I like the most in React applications is the explicitness and easiness to reason about the data flow, avoiding exactly the kind of complex algorithms that make you stop and run in your mind each step to be able to grasp what that local state is doing there. I’ll try to work on a simplification and create a PR! 🙂

    Also, is it a good idea to access the state directly from the actions — and even worse, update it, like in https://github.com/jamesknelson/raw-react-part-3/blob/master/src/actions/contact.js#L35 –?

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *