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
andprops
, and when to use each. - Refactor
ChordEditor
to utilizeprops
.
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 </td>' +
'<td class="lyrics">Lyrics </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.