What I learnt about writing React in 2017
Over the past month or so, I’ve gotten some real world experience of React in a project, where I’m replacing existing front-end code with quite a large footprint and significant complexity. I’ve previously written about my experiences with React here, here and here, but this was the first time I’d done any seriously planned, considered production-time-customers-are-going-to-use-this work with it.
For the most part, it’s been a tonne of fun. I’ve enjoyed ‘thinking in React’, and looking at the broader ecosystem with testing, deploying and adding things like Flow to the mix. I’ve also learnt a fair bit along the way, and wanted to share some little things I’ve found out from other people.
Binding and currying class functions in React components
Binding class properties in ES7
When using JSX, reasoning about the this
keyword can be hard; have a guess at what this.setState
inside onButtonClick refers to when the onClick delegate is triggered:
class MyComponent extends Component {
onButtonClick() {
this.setState({clicked: true});
}
render() {
return (
<button onClick={this.onButtonClick}>
Click me!
</button>
)
}
}
If you guessed undefined
, you’d be right! That’s because the onClick method is just a proxy for the bridge between React and the DOM onClick listener; this.onButtonClick gets executed with a different value of this.
The last time I worked with React, you had two choices:
- Bind using
() => this.onButtonClick()
in the render method; this can become quite expensive since each time you render, you’re creating a new function, with a new identity. - Reassign your event handlers and apply the
this
binding in the constructor ofMyComponent
usingFunction.prototype.bind
; this is kinda messy and scales quite poorly if you have lots of these event handling methods in your component.
With an ES7 syntax proposal, you can use class-bound properties to auto-bind the this
keyword in JSX. Example code here would be:
class MyComponent extends Component {
onButtonClick = () => {
this.setState({clicked: true});
}
render() {
return (
<button onClick={this.onButtonClick}>
Click me!
</button>
)
}
}
Note that this ES7 syntax still isn’t supported in all browsers, or even properly ratified by TC39, so you’ll still need to transpile, using something like Babel and it’s stage-2 plugin.
Bonus usage: Curried class binding methods
This strategy can be used quite effectively when you want to reuse method bindings that involve specific values, that get passed down to lower components that don’t need to know about the arguments provided.
For example, given a presentational component we render multiple times:
const MyPresentationalComponent = ({itemName, onDoTheThing}) => {
return (
<div>
<p>{itemName}</p>
<button onClick={onDoTheThing}>Buy now</button>
</div>
);
}
the following code:
class MyContainer extends Component {
doTheThing(itemName) {
// Do something neat with this
}
render() {
const items = ['diamond', 'pearl', 'platinum'];
return (
{
items.map(item => {
return (
<MyPresentationalComponent
key={item}
itemName={item}
onDoTheThing={() => doTheThing(itemName)}
/>
);
}
)
}
)
}
}
can be refactored to:
class MyContainer extends Component {
doTheThing = itemName => () => {
// Do something neat with this
}
render() {
const items = ['diamond', 'pearl', 'platinum'];
return (
{
items.map(
item => <MyPresentationalComponent
key={item}
itemName={item}
onDoTheThing={doTheThing(itemName)}
/>
)
}
)
}
}
which is more readable and concise. This pattern is even more beneficial if event data matters to you (for example, if you’re entering text into an input box):
class MyContainer extends Component {
doTheThing = itemName => event => {
// Do something neat with the item name and the event payload,
// which will both be passed up the stack.
}
render() {
const items = ['diamond', 'pearl', 'platinum'];
return (
{
items.map(
item => <MyPresentationalComponent
key={item}
itemName={item}
onDoTheThing={doTheThing(itemName)}
/>
)
}
)
}
}
Don’t use “export default”
I was thrown by this one, since create-react-app
is instructive in using export default
for exporting components. But, as it turns out, there’s a few maintainability concerns around using export default
for exporting components. Better minds than I have explained this, and there are actually like six reasons, so check out the concerns here.
Use the functional version of setState() for form validations
Most web apps you write will use forms. Most forms have multiple input fields, and require validation. Some of these inputs even have validation that depend on one another.
I was scratching my head a little bit, trying to work out how best to perform field validation within the unidirectional flow of React. My solution came down to:
- Keep validation out of the render cycle
- Set field state at an individual level (you can reuse onX() methods, but some of them have specific handling requirements)
- After each field state mutation, call a validate-all-the-things method that performs asynchronous setState() validation
Here’s a sample:
class MyWonderfulForm extends Component {
render() {
return (
<form>
<input type="text" name="Name" onChange={this.onNameChange} />
<input type="text" name="Address" onChange={this.onAddressChange} />
<input type="text" name="CreditCardNumber" onChange={this.onCreditCardNumberChange}
<button disabled={!canSubmit}>Submit</button>
</form>
)
}
onNameChange = event => {
this.setState({name: event.target.value});
this.onPerformValidation()
}
onAddressChange = event => {
this.setState({address: event.target.value});
this.onPerformValidation()
}
onCreditCardNumberChange = event => {
this.setState({creditCardNumber: event.target.value});
this.onPerformValidation()
}
onPerformValidation = () => {
// If you wanted to, you could disable form input here until the below setState resolves.
this.setState((currentState, props) => {
// Perform your intensive validation here
const creditCardNumberOk = luhn(currentState.creditCardNumber);
const nameOk = currentState.name.length > 0;
const addressOk = currentState.address.length > 0;
const canSubmit = creditCardNumberOk && nameOk && addressOk;
return {
canSubmit,
validation: {
creditCardNumber: creditCardNumberOk,
name: nameOk,
address: addressOk
}
}
});
}
}
As you can see, this.setState can optionally take an anonymous function, with the args of currentState
and props
. The call gets scheduled to occur after all other existing setState() merges have occurred; thus, it will always be up to date with the latest form field variables.
This is also important for users rapidly typing into a form, or changing multiple inputs, so that immediate text entry updates aren’t blocked, especially if you have any kind of intensive (or asynchronous) validation.
React-based unit testing is a cinch with Enzyme
Nobody really likes automated end-to-end testing; these tests are painful to write, slow to run, often quite flaky, and have many transitive dependencies, so they’re hard to easily simulate locally. They’re the least likely tests for developers to run before merging their code.
Since most of your app front-end components are React components, and they are set up to take props and make observable, predictable state changes, it’s actually really easy to facilitate testing of interactivity and different flows of your application. Whilst they’re not the same as end-to-end tests, it’s easier to increase overall interactivity coverage (which arguably is the most important to your end-user).
You can do these simulations with React, an expectations framework and jsdom, but Airbnb have written a wrapper around jsdom and React called Enzyme. Enzyme makes it easier to render out components (including shallow renders that allow for simpler tests), and test effects of user interactions that get passed through to React and may change state. The docs describe it as ‘jQuery style’ interrogation and inspection of the elements, and this certainly rings true.
I want to go through some common React unit testing patterns in greater detail in a later post, but I’ll describe a quick example.
Given this simple interactive component:
// Has a bunch of details about the selected plan, and a link to go back to the previous screen.
class PlanDetailsPane extends Component {
render() {
const {onBackClick} = this.props;
<div>
<p>Something something something.</p>
<a className={'click-to-go-back'} onClick={onBackClick}>
Something something blah
</a>
</div>
}
}
you can test a simple interaction, using your standard JS unit testing libraries, a bit of Sinon stubbing and the Enzyme library.
import {shallow} from 'enzyme';
import expect from 'expect';
import {PlanDetailsPane} from './PlanDetailsPane';
import sinon from 'sinon';
describe('<PlanDetailsPane />', () => {
it('should call onBackClick when the link is clicked', () => {
const onBackClick = sinon.spy();
const wrappedComponent = shallow(<PlanDetailsPane onBackClick={onBackClick} />);
wrappedComponent.find('a').simulate('click');
expect(onBackClick.called).toEqual(true);
});
});
Again, this type of testing doesn’t replace the role of end-to-end tests (you should still do these type of tests before deploying your code to production :)) but it can help catch errors and give quick feedback to you and future devs. It also acts as de-facto component documentation.