Rock and Roll React.js Tutorial – Part 3

Welcome to Rock and Roll React.js Tutorial – Part 3.

If you’ve been following along, you’ll know that I’m trying to stretch myself. I’m trying to build bigger and more complex apps. For this second tutorial, I wanted an app that:

With huge thanks to Balint Erdi for giving me permission. I think I found the perfect app to base this tutorial on. We’re building a React.js equivalent of the Rock and Roll App built in Balint’s Ember.js tutorial https://www.toptal.com/javascript/a-step-by-step-guide-to-building-your-first-ember-js-app.

In part one, after designing our component breakdown we setup the building blocks. We have configured React-Router so the app uses URLs /bands and /bands/<bandid>/songs. We also built shell components for our top-level components. In this part, we’re going to flesh out the functionality for the <BandList /> component

In part two, we completed the left-hand side of our app. We also seeded Redux with some initial state. Created a <BandsList /> component that displayed the current bands. Finally, we created a <NewBand /> controlled component. We connected this last component to Redux to create new bands.

In this part, we’re going to finish our app by creating all the functionality of the right-hand side. There are a couple of similar topics, so I’ll be moving quite fast. For example, I won’t explicitly list the actions and action creators for adding and rating a song. If you find it too fast, please leave a comment below.

Display Songs for an artist

While building this bit, I got it working, but the <SongList /> component was a mess. I decided to refactor it into several other components. The hierarchy is now:

  • SongContainer
    *NoBandSelected

    • NewSong
    • SongRow
    • StarRating
    • NoSongs – a band without any songs. This is displayed instead of StarRow

<NoBandSelected /> is shown when, you guessed it, no band is selected. It shows the text “Select a Band”. Create a file called NoBandSelected.js in src\components

import React, {Component, PropTypes} from 'react';

class NoBandSelected extends Component {
    render() {
        return (
            <div className="list-group">
                <div className="list-group-item empty-list" style={{ minHeight: "100px", textAlign: "center" }}>
                    <div className="empty-message" style={{ display: "inline-block", color: "#555", lineHeight: "100px" }}>
                        Select a band.
                    </div>
                </div>
            </div>
        );
    }
}

export default NoBandSelected;

We render <NoSongs /> instead of <SongRow /> if there are no songs. Create a file called NoSongs.js in src\components

import React, {Component, PropTypes} from 'react';

class NoSongs extends Component {
    render() {
        return (
            <div className="list-group-item empty-list" style={{ minHeight: "100px", textAlign: "center" }}>
                <div className="empty-message" style={{ display: "inline-block", color: "#555", lineHeight: "100px" }}>
                    There are no {this.props.bandName} songs.Why don't you create one?
                </div>
            </div>
        );
    }
}

export default NoSongs;

I think this breakdown makes the <SongsContainer /> code easier to read. Let me know if there’s a way of tidying it up further.

Let’s create that now.

SongsContainer Component

There is a lot of code in <SongsContainer />. It’s possible to split the code using Presentational and Container Components. But for now, I’m leaving it as one file.

import { connect } from 'react-redux';
import {bindActionCreators} from 'redux';
import React, {Component, PropTypes} from 'react';
import SongRow from './SongRow';
import NewSong from './NewSong';
import NoBandSelected from './NoBandSelected';
import NoSongs from './NoSongs';
import * as actions from '../actions/rockAndRollActions';

function starClicked(bandId, ratingHandler, songId, starNumber) {
    ratingHandler(bandId, songId, starNumber);
}

const SongsContainer = ({bandName, songs, params, actions}) => {

    const handleAdd = (newSongName) => {
        actions.addSong(params.bandid, newSongName);
    }

    if (bandName === "") {
        return <NoBandSelected />
    }

    else {
        return (
            <div className="list-group">
                <NewSong
                    key={bandName + "Holder"}
                    bandName={bandName}
                    handleAdd={handleAdd}
                    />

                {songs.length > 0 ?
                    songs.map(song =>
                        <SongRow
                            key={song.id + "_" + song.title}
                            song={song}
                            starClicked={starClicked.bind(this, params.bandid, actions.rateSong) } />) :
                    <NoSongs
                        bandName={bandName} />}
            </div>
        );
    }
}

const mapStateToProps = (state, ownProps) => {
    return {
        songs: getSongs(state, ownProps.params.bandid),
        bandName: getBandName(state, ownProps.params.bandid)
    };
};

const mapDispatchToProps = (dispatch) => {
    return {
        actions: bindActionCreators(actions, dispatch)
    }
}

function getSongs(state, bandid) {
    if (bandid) {
        return state.bands[bandid].songs;
    }

    return [];
}

function getBandName(state, bandid) {
    if (bandid) {
        return state.bands[bandid].name;
    }

    return "";
}

export default connect(mapStateToProps, mapDispatchToProps)(SongsContainer);

The new technique in this component is in the rendering. JSX doesn’t handle if…else statements https://facebook.github.io/react/tips/if-else-in-JSX.html. But you can work around that by using the ternary operator. That’s exactly what I’ve done inside the return statement. We either map across the songs associated with the selected band. Or we display <NoSongs /> if there are none.

Calculating which band is currently selected is new. As that information is in the URL, :bandid, we don’t want to store that inside our Redux store. Otherwise, we have two sources of truth and would have to put quite a lot of effort into syncing them.

Instead, we use the ownProps parameter to mapStateToProps. As the name suggests. This parameter contains all the props that are passed to the current component. This means we can access the ReactRouter params object. Which is exactly what we do.

In the current state, the app won’t compile as we’re missing <NewSong /> and <SongRow />. Let’s create these now.

Add New Song Component

As I mentioned in the introduction, this is just like adding a band. The component itself resides in src\components\NewSong.js

import React, {Component, PropTypes} from 'react';

class NewSong extends Component {

    constructor(props) {
        super(props);
        this.state = { _input: '' };
        this.handleChange = this.handleChange.bind(this);
        this.handleAdd = this.handleAdd.bind(this);
    }

    handleChange(e) {
        this.setState({ _input: e.target.value })
    }

    handleAdd(e) {
        event.preventDefault();
        this.props.handleAdd(this.state._input);
        this.state = { _input: '' };
    }

    render() {
        return (
            <div className="list-group-item">
                <input
                    placeholder="New Pearl Jam song"
                    type="text"
                    style={{ width: "70%" }}
                    value={this.state._input}
                    onChange={this.handleChange}
                    />
                <button
                    className="btn btn-primary btn-sm"
                    style={{ marginLeft: "20px" }}
                    disabled={!this.state._input}
                    onClick={this.handleAdd}
                    >
                    Add
                </button>
                <select className="pull-right">
                    <option value="rating:desc,title:asc">Best</option>
                    <option value="title:asc">By title (asc) </option>
                    <option value="title:desc">By title (desc) </option>
                    <option value="rating:asc">By rating (asc) </option>
                    <option value="rating:desc">By rating (desc) </option>
                </select>
            </div>
        );
    }
}

export default NewSong;

With that complete, let’s create a <SongRow /> component.

Song Row Component

Again, there are no new techniques here. The component resides in src\components\SongRow.js

import React, {Component, PropTypes} from 'react';

class SongRow extends Component {

    constructor(props) {
        super(props);
    }

    render() {
        let song = this.props.song;
        let starClicked = this.props.starClicked;

        return (
            <div className="list-group-item">
                {song.title}
                <div style={{ display: "inline-block", float: "right" }}>
                    Star Rating Component Here
                </div>
            </div>
        );
    }
}

export default SongRow;

There’s some placeholder text for the <StarRating /> component. We’ve made a lot of changes without seeing the app running. I don’t want to go too far down a path of not knowing if I’ve broken anything.

So run the app and click a band. You should see something like this:

Rock and Roll Part 3 - With Songs

Rock and Roll Part 3 – With Songs

So let’s add ‘`.

Star rating widget

I found this part tricky. We need to pass information from <StarRating /> to <SongRow /> to <SongsContainer />'. The eagle-eyed might have seen how I accomplished this already. But let's look at` first.

The star rating widget is something I really wanted to make as re-usable as possible. This means, all the details the component needs to render should be passed in via props.

Just like the Ember app, we are passing down:

  • title – used for the “key” which React needs. “title” is a poor name
  • rating – the rating to render, i.e. the number of stars to fill in
  • maxRating – the total number of stars to render
  • handleClick – the click event handler to call.

Create a new file, src\components\StarRating.js for this component.

import React, {Component, PropTypes} from 'react';

class StarRating extends Component {

    constructor(props) {
        super(props);

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

    renderStars(rating, maxRating, handleClick, title){
        let i = 0;
        let result = [];
        let className = "star-rating glyphicon glyphicon-star";

        for (i = 0; i < maxRating; i++) {
            result.push(<span
                key={title + "_" + i}
                className={className + (i >= rating ? "-empty" : "") }
                onClick={handleClick.bind(this, i + 1) }>
            </span>);
        }

        return result;
    }

    render () {
        let {rating, maxRating, handleClick, title} = this.props;
       
        return <div>{this.renderStars(rating, maxRating, handleClick, title)}</div>;
    }
}

export default StarRating;

Then update the <SongRow /> code uses it instead of the holder text.

<StarRating
                        title={song.title}
                        rating={song.rating}
                        maxRating={5}
                        handleClick={starClicked.bind(this, song.id)}
                        />

Now we need to handle a user clicking on a star.

Star Clicked Handling

We have 3 components in our chain to handle a star being clicked.

At the bottom, we have <StarRating />. When a star is clicked, we need to capture the number clicked. The click handler is passed into it from <SongRow /> via props.

<SongRow /> doesn’t know what band it is associated with. The only context it can provide is the SongId. So the actual click handler resides in the top component, <SongContainer />.

<SongContainer /> is able to provide the final piece of the puzzle, BandId.

This picture shows what information is available at each layer

Rock and Roll React - Part 3 - JavaScript Binding

Rock and Roll React – Part 3 – JavaScript Binding

To achieve this, I’m using the JavaScript bind prototype. From that link:

The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called

Or in other words, the method called will have an extra parameter of the value you’ve bound to it. Looking back through the code, you can see the parameter list growing as you go up the layers.

Now we know the relevant information available in <SongContainer />. We need to update the state.

Updating the state

I chose not to normalize the state in part 1. I think that was a mistake as it made this part harder than it could have been. The upside of that decision though was that I learned about Immutability Helpers.

Again, I won’t show the code for hooking up the actions. But I will show the final code for our reducer – src\reducers\BandsReducer.js.

import initialState from './initialState';
import * as types from '../constants/actionTypes';
var update = require('react/lib/update')

export default function BandsReducer(state = initialState, action) {
  const length = state.length;

  switch (action.type) {
    case types.ADD_BAND:
      return [...state,
        {
          id: length,
          name: action.newBandName,
          songs: []
        }
      ]
    case types.RATE_SONG:
      let newSong = Object.assign({},
        state[action.bandId].songs[action.songId],
        { rating: action.newRating });
      return update(
        state,
        {
          [action.bandId]: {
            songs: {
              [action.songId]: {
                $merge: { rating: action.newRating }
              }
            }
          }
        }
      )
    case types.ADD_SONG:
      let newIndex = state[action.bandid].songs.length;

      return update(
        state,
        {
          [action.bandid]: {
            songs: {
              $push: [{ id: newIndex, title: action.newSongName, rating: 0 }]
            }
          }
        }
      );
    default:
      return state;
  }
}

As you can see in the case types.RATE_SONG. I use the immutability helper method update. I admit that it was a little tricky to get my head around, but once I did, completing the reducer was possible.

App Complete!

If you run the app now, you should see Rock and Roll React.js complete. Try adding a band, adding a song and rating it.

Rock and Roll React - Part 3 - Finished App

Rock and Roll React – Part 3 – Finished App

I learned a lot creating this app and I hope you enjoyed following along. My initial component breakdown was close. Also, the state design didn’t hurt us too much. I think in hindsight, normalizing the data would have made the reducer simpler. But then I would not have learned about the immutability helpers.

Finally, huge thanks to Balint Erdi. Granting permission and his generosity in tweeting about this tutorial is appreciated.

As always, if you have any comments, questions or problems. Please let me know below or contact me on twitter.

No comments yet, your thoughts are welcome.

Leave a Reply

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