Getting Started With React Router

Tue Jul 4, 2017 - 1100 Words

In a “single page application” it might make sense only to have one page, but it is unlikely. We’re only rendering a single page to the browser, but we need to be able to show different views. Today, we’re going to utilize React Router to add URL handling and separate views to our application.

Goals

  • Install React Router.
  • Create song selection view.

We want our user’s to be able to work on more than a single song, and for that, we need to have the ability to view a list of songs. If we were working on a traditional, server-rendered web application, we would have different URLs for these separate views, and we’ll do the same here.

Installing React Router

To install React Router, we’re going to install the react-router-dom package.

$ yarn add react-router-dom

Now that we have our new dependency installed it’s time to use it.

Defining a Route for the ChordEditor

React Router is a fascinating package. If you’ve worked with a framework like rails, then you would define your routes in terms of the HTTP verb and the path, but that’s not how it will work with React Router. That makes sense since everything is happening within the same HTML page, but things get weird when you think about where you define the routes.

With React Router you define your routes by using a component so it looks like it would be markup. Let’s create the router without routes. First, we need to import our first component from React Router, BrowserRouter:

src/App.js

// Other imports omitted
import { BrowserRouter } from 'react-router-dom';

Next, we’ll utilize this in our render function:

src/App.js

// only showing App's render function
render() {
  return (
    <div className="wrapper">
      <Header />
      <div className="workspace">
        <BrowserRouter>
          <div className="main-content">
            <ChordEditor song={this.state.song} updateSong={this.updateSong}/>
          </div>
        </BrowserRouter>
      </div>
      <Footer />
    </div>
  );
}

If we reload the page now, you should see that nothing changed, and that’s because we didn’t create any routes. Similar to a div if you wrap markup in a BrowserRouter it will render as expected.

We want to render our ChordEditor component with a particular song and let’s say we utilize a URL like /songs/:songId. We’ll also want to list all songs on the root (/) and /songs routes. We’ll utilize the Route component from React Router to do this.

src/App.js

// Other imports omitted
import { BrowserRouter, Route } from 'react-router-dom';

Since we want to start working with multiple songs, we need to change the state of our application to have a songs value instead of a song, and this is going to change how we need to update our song.

src/App.js

class App extends Component {
  constructor() {
    super();
    this.updateSong = this.updateSong.bind(this);
    this.state = {
      songs: {
        "1": { id: 1, chordpro: "Test Song Number One" },
        "2": { id: 2, chordpro: "Test Song [B]number Two" }
      }
    };
  }

  updateSong(song) {
    const songs = {...this.state.songs};
    songs[song.id] = song

    this.setState({songs});
  }

  // render omitted
}

Notice that the updateSong function has us doing something a little weird. Since we only want to update one of the items in a list we make a copy of the song list, update the song, and then update our state with the new song list. This approach gives us a chance to bail out if something goes wrong since we’re not setting our state through a setter and only use setState.

Now we can wrap our ChordEditor component in a Route.

src/App.js

// omitting all but render
render() {
  return (
    <div className="wrapper">
      <Header />
      <div className="workspace">
        <BrowserRouter>
          <div className="main-content">
            <Route path="/songs/:songId" render={(props) => {
              const song = this.state.songs[props.match.params.songId];
              return (
                song
                ? <ChordEditor song={song} updateSong={this.updateSong} />
                : <h1>Song not found</h1>
              )
            }} />
          </div>
        </BrowserRouter>
      </div>
      <Footer />
    </div>
  );
}

The Route component automatically passes a few things into its render function as props. One of those is the match which let’s us have access to the tokens parsed out of the URL. We’re going to utilize that match to get the songId and then find the song that matches that. If there is a song with the specified id then we’ll render our chord editor; otherwise, we render a poor man’s 404 page.

When you to go /songs/1 you’ll see our song, but when you try to edit it you won’t see anything happen and that’s because we’ve been a little too explicit with how we utilize updateSong within the ChordEditor component. We’ll use a similar copy and modify strategy to pass an entire song back to the updateSong function:

src/components/ChordEditor.js

// only showing `handleChange`
handleChange(event) {
  const song = {...this.props.song};
  song.chordpro = event.target.value;

  this.props.updateSong(song);
}

Rendering the Song List

The last thing that we need to do is render a simple list of songs that link to the individual song pages. We won’t create a full component for this; we’ll instead iterate over the songs from within our the new route’s render function. Don’t worry; this will be extracted out at a later point. As with before we do need to pull in another React Router component, Link.

src/App.js

import { BrowserRouter, Route, Link } from 'react-router-dom';

Here’s our new route:

src/App.js

// only showing the render function
render() {
  return (
    <div className="wrapper">
      <Header />
      <div className="workspace">
        <BrowserRouter>
          <div className="main-content">
            <Route exact path="/songs" render={(props) => {
              const songIds = Object.keys(this.state.songs)
              return (
                <ul>
                  {songIds.map((id) => {
                    return (
                      <li key={id}>
                        <Link to={`/songs/${id}`}>Song {id}</Link>
                      </li>
                    )
                  })}
                </ul>
              )
            }} />
            <Route path="/songs/:songId" render={(props) => {
              const song = this.state.songs[props.match.params.songId];
              return (
                song
                ? <ChordEditor song={song} updateSong={this.updateSong} />
                : <h1>Song not found</h1>
              )
            }} />
          </div>
        </BrowserRouter>
      </div>
      <Footer />
    </div>
  );
}

Now when you go to the /songs path, you will see a clickable list of songs that will get you into the edit screen for each.

Recap

We accomplished a lot in this tutorial. The application can now handle multiple songs and has a song listing page. Having routes allows us to have an application that behaves as expected where there is a URL for the resources that our users will interact with (so they can link to them for instance).