How to Use React State and Props

Tue Jun 20, 2017 - 1300 Words

In the previous tutorial, we looked at how to test the ChordEditor component, and it showed that we could probably have designed the component better. In this tutorial, we will improve the implementation of our component by looking at the differences between “state” and “props” in React.

Goals

  • Remove warnings from test runs.
  • Learn about state and props, and when to use each.
  • Refactor ChordEditor to utilize props.

The main issue that we ran into while testing was that we couldn’t manipulate the output of our chord chart when initially creating an instance of the component. The text for the output would always be “Type some lyrics here”.

Removing Warnings from Test Runs

Before we get into the meat of this tutorial, we should fix the errors shown when we ran the tests. We followed the wrong installation instructions for enzyme. We need to remove the react-addons-test-utils from our package.json, and instead install react-test-render. While we’re messing with our dependencies we’ll also get react and react-scripts up-to-date.

$ yarn remove react-addons-test-utils
$ yarn upgrade chordsheetjs react react-dom react-scripts
$ yarn add react-test-renderer --dev

Now we’re running with the newest of dependencies and when we run CI=true npm test you will see that all of the tests pass.

State Vs Props

We haven’t looked at props yet other than when we’ve implemented the constructor in a few of our components. Here’s the implementation from ChordEditor:

src/components/ChordEditor.js

class ChordEditor extends Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = { value: '' };
  }
  // truncated
}

The props are passed in when creating a component, and you set them in the same way that you would set an attribute on an HTML element. Here’s what the usage of ChordEditor will look like when we’re done:

<ChordEditor song={{chordpro: '[B]Some [A]Chords'} updateSong={someFunction}} />

The chord chart will no longer be part of the ChordEditor state. ChordEditor will be in charge of displaying the formatted output, rendering a textarea, and notifying it’s parent of the changes to the song.

In React, the state is something that internal to the component, in our case the HTML output will be part of the state. On the other hand, props contain the data the configurable data of the component. We eventually want to be able to swap out the song being edited, so it makes sense to have the song/chordpro be something that we pass into the component to render.

Refactoring to Use Props

First, we’re going to switch over to using props in our tests. One of the benefits of testing is that you can use your tests to show what the proper use of a library/function/component should be. Let’s change the test to show how we want ChordEditor to work:

src/components/ChordEditor.test.js

import React from 'react';
import { shallow } from 'enzyme';
import ChordEditor from './ChordEditor';

describe('<ChordEditor />', () => {
  it('renders an editor area', () => {
    const editor = shallow(<ChordEditor song={{chordpro: ""}}/>);
    expect(editor.find('textarea').length).toEqual(1);
  });

  it('renders an output area', () => {
    const editor = shallow(<ChordEditor song={{chordpro: ""}}/>);
    expect(editor.find('div.chord-output').length).toEqual(1);
  });

  it('renders the chord chart output', () => {
    const editor = shallow(<ChordEditor song={{chordpro: '[B]New [Am]Lyrics'}}/>);
    const expectedOutput =
      '<table>' +
      '<tr>' +
      '<td class="chord">B</td>' +
      '<td class="chord">Am</td>' +
      '</tr>' +
      '<tr>' +
      '<td class="lyrics">New&nbsp;</td>' +
      '<td class="lyrics">Lyrics&nbsp;</td>' +
      '</tr>' +
      '</table>';

    const realOutput = editor.find('div.chord-output').html();
    expect(realOutput.indexOf(expectedOutput) > -1).toEqual(true);
  });
});

Since the ChordEditor will only be concerned with displaying something passed in, we didn’t need to have two test for the rendering. We’ve deleted the original “Type some lyrics” version in favor of the test that used Chordpro notation. We also removed the setState call from the test since we won’t rely on the state. Running the tests, you will see a failure.

Let’s change the implementation to get this test to pass again:

src/components/ChordEditor.js

import React, { Component } from 'react';
import ChordSheetJS from 'chordsheetjs';

class ChordEditor extends Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

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

  getChordMarkup() {
    const formatter = new ChordSheetJS.HtmlFormatter();
    const parser = new ChordSheetJS.ChordProParser();
    const song = parser.parse(this.props.song.chordpro);

    return { __html: formatter.format(song) };
  }

  render() {
    return (
      <div className="chord-editor">
        <div className="panel">
          <h3>Input</h3>
          <textarea
            style={{width: "100%", height: "100%"}}
            onChange={this.handleChange}
            value={this.props.song.chordpro}/>
        </div>
        <div className="panel">
          <h3>Output</h3>
          <div
            style={{width: "100%", height: "100%", fontFamily: "monospace"}}
            className="chord-output"
            dangerouslySetInnerHTML={this.getChordMarkup()}/>
        </div>
      </div>
    );
  }
}

export default ChordEditor;

This changes all of the places that previously had this.state.value to be this.props.song.chordpro, and also changed the textarea from using defaultValue to using value. Now the tests for the ChordEditor are passing, but we broke the application. There is no longer anything useful happening onChange. We need to update the state at a higher level and let that trigger a redraw of our ChordEditor. Let’s write a test to demonstrate this:

src/components/ChordEditor.test.js

it('calls updateSong when the the textarea changes', () => {
  var theSong
  const update = (song) => {
    theSong = song;
  };

  const editor = shallow(<ChordEditor song={{chordpro: '[B]New [Am]Lyrics'}} updateSong={update}/>);

  editor.find('textarea').simulate('change', { target: { value: '[B]New [Am]Lyrics ' }});
  expect(theSong).toEqual({chordpro: '[B]New [Am]Lyrics '});
});

We’re creating an anonymous function to handle the “updating” of a song that we’ll also inject as a prop and set theSong to be the object that we’ll check at the end. By injecting the updateSong function, we are making it flexible enough to handle being used in any container that has a function to update its state.

Next, we utilize the simulate function from enzyme to fake a change event. simulate doesn’t trigger a real event, so we have to create a fake one to pass into the updateSong function. Since we know an event will respond to .target.value. Finally, to ensure that we call the function we make an assertion against the theSong variable.

Let’s change ChordEditor to get this test to pass by changing the handleChange function:

src/components/ChordEditor.js

  handleChange(event) {
    const chordpro = event.target.value;

    this.props.updateSong({
      chordpro: chordpro
    });
  }

When you run the tests again, you will see that the ChordPro tests are all passing, but now the App tests are failing.

Passing in State from App

We utilize the ChordEditor component from within our App component, so that’s where we will inject the song and updateSong props. We’ll do this by defining and binding a new updateSong method to our component to pass along, and we’ll set up some initial state too.

src/components/App.js

import React, { Component } from 'react';
import Header from './components/Header';
import Footer from './components/Footer';
import ChordEditor from './components/ChordEditor';

class App extends Component {
  constructor() {
    super();
    this.updateSong = this.updateSong.bind(this);
    this.state = {
      song: { chordpro: "Type lyrics here." }
    };
  }

  updateSong(song) {
    this.setState({ song: song });
  }

  render() {
    return (
      <div className="wrapper">
        <Header />
        <div className="workspace">
          <ChordEditor song={this.state.song} updateSong={this.updateSong} />
        </div>
        <Footer />
      </div>
    );
  }
}

export default App;

Now if you manually test the application you should see that it works as it did before. The tests are passing, and we’ve successfully improved our ChordEditor component.

Recap

In this tutorial, we learned a little more about how data should flow through a React application. The refactoring we just did was necessary for us to be able to have a collection of songs that we can then select, edit, and update from within the application.