Rock and Roll React.js Tutorial – Part 2

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

In Part 1 I explained that I’m trying to stretch myself to build bigger and more complex apps. 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.

Last time, 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’ll flesh out the functionality for the <BandList /> component.

But before we start coding, let’s design the state of the app. Note I’m saying app, not components. Some components might need a local state, but I won’t know that till I start coding.

Designing the Application State

I’ll be using Redux again to store the application state. This app doesn’t need Redux, but it will build on the skills we’ve learned from last time.

Let’s consider the data we’re storing. We have a list of bands. Each band can have a list of songs. Songs have a title and rating. To keep it realistic, every band and song should have an id.

My initial thought is something like:

{
    bands: [
        {
            id
            name
            songs:[{
                id
                title,
                rating
        }]
    }]
}

If this is right, I think we’ll need two nested reducers. BandsReducer and SongsReducer to manage the bands and songs respectively.

The Redux documentation recommends normalized data over deeply nested data. I think this recommendation is so the data more closely represents a database. I’m not worried about realistic data storage at the moment, so I think we’ll just start and see how we get on.

Before we flesh out our <BandList /> component, let’s add some data to our app.

Store some data in Redux to display

Like we did in the previous tutorial’s “Designing Application State”, create a file in src\reducers called initialState.js:

export default [
    {
        id: 0,
        name: 'Pearl Jam',
        songs: [
            { id: 0, title: 'Daughter', rating: 5 },
            { id: 1, title: 'Yellow Ledbetter', rating: 5 },
            { id: 2, title: 'Animal', rating: 4 },
            { id: 3, title: 'Inside Job', rating: 4 },
            { id: 4, title: 'Who We Are', rating: 2 }
        ]
    },
    {
        id: 1,
        name: 'Led Zeppelin',
        songs: [
            { id: 0, title: 'Black Dog', rating: 4 },
            { id: 1, title: 'Achilles Last Stand', rating: 5 },
            { id: 2, title: 'Immigrant Song', rating: 4 }
        ]
    },
    {
        id: 2,
        name: 'Foo Fighters',
        songs: [
            { id: 0, title: 'Pretender', rating: 3 }
        ]
    },
    {
        id: 3,
        name: 'Radiohead',
        songs: [
        ]
    }
];

As you can see, it’s the bands and songs from the Ember.js app, but using the structure we designed above. Note, I’ve cheated a little and made the id fields equal to their position in the array. As we’re only every creating new bands we can use the array.length property to assign new ids. This would not work if we were deleting items.

Again, I’m not trying to learn correct data storage in this tutorial. I’m trying to build our knowledge with React-Router.

Creating a BandsReducer

Create a new file called BandsReducer.js in src/reducers. As we’re not performing any operations we’ll just use the initialState file.

import initialState from './initialState';

export default function BandsReducer(state = initialState, action) {
  switch (action.type) {
    default:
      return state;
  }
}

Use this reducer in our store

The boilerplate we’re using makes this simple. Edit src\reducers\index.js to

import { combineReducers } from 'redux';
import bands from './BandsReducer';

const rootReducer = combineReducers({
  bands
});

export default rootReducer;

This mean our array of bands from initialState.js is at state.bands. We’ll use this information when we connect to a component in a moment.

Display the list of bands

If you remember from part 1, we broke down our components for displaying bands to:

  • “BandsList” top-level component
    • “NewBand” component
    • “BandRow” component

Update the <BandList /> shell from part 1 to:

import { connect } from 'react-redux';
import React from 'react';
import BandRow from './BandRow';

const BandsList = ({bands}) => {
    return (
        <div className="list-group">
            {
                bands.map(band =>
                    <BandRow key={"band" + band.id} band={band} />)
            }
        </div>
    );
}

const mapStateToProps = (state) => {
    return {
        bands: state.bands,
    };
};

export default connect(mapStateToProps)(BandsList);

Here we have import connect from Redux so we can access the state. We have also imported another component called <BandRow /> which we will import next.

Line 5 is destructuring this.props.bands to a variable called bands. It’s a little less to type and easier to read. Line 16-22 is where mapStateToProps is created. If you don’t understand this, please refer to my previous tutorial.

Lines 7-12 we map over the array, creating a new <BandRow /> for each element.

Create a new file in src\components called BandRow.js so we can see the list of bands.

import React, {Component, PropTypes} from 'react';
import { Link } from 'react-router';

const BandRow = ({band}) => (
    <Link to={"/bands/" + band.id + "/songs"} className="list-group-item band-link" activeClassName="active">
        {band.name}
        <span className="pointer glyphicon glyphicon-chevron-right" style={{display:"block", float:"right"}}></span>
    </Link>
);

export default BandRow;

I’ve highlighted line 2 and 5 as this is the part where we use React-Router. We’re using the <Link /> component again. This time, we’re using the “activeClassName” property. As the name suggests, when React-Router matches the URL to that of the to property. React-Router will also add the class specified.

Again, I’ve taken the styling from the Ember.js app. That’s bootstrap so we don’t have to edit .css files, but the principles are the same.

If you run the app now, you should see the list of bands. Try clicking one and see the styling change. You should see this.

Rock and Roll Part 2 - Band List - Band Selected

Rock and Roll Part 2 – Band List – Band Selected

Create an artist

Before we proceed to show the songs associated with a band, let’s create a new one.

Part 1 – Data Binding – Controlled Components

If you play with the Ember app, you will see that the Add functionality has two nice behaviours:

  1. It is disabled if there is no text in the input field
  2. Once you add a new band, the text field is reset back to nothing

You can do this in React using component state and a Controlled Component. If you don’t know what they are, definitely read that link. The input field in <NewBand /> will be a controlled component.

I’ll show the whole code for src\components\NewBand.js first. Then talk through which bits do what.

import React, {Component} from 'react';

class NewBand 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 band"
                    type="text"
                    style={{ width: "70%" }}
                    value={this.state._input}
                    onChange={this.handleChange}
                    />
                <button
                    className="btn btn-primary btn-sm new-band-button"
                    style={{ marginLeft: "2em", padding: "3px 10px" }}
                    disabled={!this.state._input}
                    onClick={this.handleAdd}
                    >
                    Add
                </button>
            </div>
        );
    }
}

export default NewBand;

1. Disabling the Add button

The input is a controlled component as it gets its value from state (line 30). Whenever a key is pressed, the state is set to the value of the field (handleChange function).

Line 36 is where the state of this <NewBand /> component is used. We calculate whether the button is disabled. As you can see, the disabled attribute is true if there is not a value in this.state._input. Otherwise, it’s false.

2. Resetting the input field

The onClick event of the button (line 37) is handled by the handleAdd function (line 17). The last line in handleAdd performs the reset. As you can see, it sets the state of _input back to an empty string.

To keep this component reusable, the action handling of the click is passed in via this.props.handleAdd. Let’s add this component to <BandsList /> and see how that works.

Part 2 – Action Handlers – Redux Actions

I covered Redux action creators in my GitHub Trending Repos Tutorial so won’t go into detail how they work here. If you want to follow along, create the following:

src\constants\actionTypes.js

export const ADD_BAND = "ADD_BAND";

src\actions\rockAndRollActions.js

import * as types from '../constants/actionTypes';

export function addBand(newBandName) {
    return {
        type: types.ADD_BAND,
        newBandName
    };
}

We then need to update our reducer to handle the ADD_BAND action.

import initialState from './initialState';
import * as types from '../constants/actionTypes';

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: []
        }
      ];
    default:
      return state;
  }
}

Lines 8-15 we’re using array spread syntax to append a new band onto the end of the current state.

All that remains is to hook this into our <BandsList /> component.

import { connect } from 'react-redux';
import React from 'react';
import BandRow from './BandRow';
import NewBand from './NewBand';
import {bindActionCreators} from 'redux';
import * as actions from '../actions/rockAndRollActions';

const BandsList = ({bands, actions}) => {
    const handleAdd = (newBandName) => {
        actions.addBand(newBandName);
    }

    return (
        <div className="list-group">
            <NewBand handleAdd={handleAdd} />
            {
                bands.map(band =>
                    <BandRow key={"band" + band.id} band={band} />)
            }
        </div>
    );
}

const mapStateToProps = (state) => {
    return {
        bands: state.bands,
    };
};

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

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

Nothing new there, just your standard mapDispatchToProps like last time. Run the app and you should be able to add a new Band.

Rock and Roll Part 2 - New Band Added

Rock and Roll Part 2 – New Band Added

End of Part 2

That’s quite a long tutorial. But we have now completed the functionality of the left-hand side of the app. We’ve crossed off the wish list. We have multiple routes (I thought they would be pages but not to worry). There is a simple crud operation and finally, we have a more complex UI.

There’s a bit more left to do, though. Next time we’ll add the functionality of the right-hand side. We’ll be displaying the list of songs, be able to add new songs and finally rating songs.

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 *