React
Mostly reasonable patterns for writing React Component in 2017
Table of Contents
- Scope
- Organization
- Component Organization
- Formatting Props
- Patterns
- Computed Props
- Compound State
- prefer-ternary-to-sub-render
- View Components
- Container Components
- Anti-patterns
- Compound Conditions
- Cached State in render
- Existence Checking
- Setting State from Props
- Practices
- Naming Handle Methods
- Naming Events
- Using PropTypes
- Using Entities
- Gotchas
- Tables
- Libraries
- classnames
Scope
This is forked from [https://github.com/khakurel/react-patterns] to meet our requrement
All examples written in ES2015 syntax with transform-class-properties [https://babeljs.io/docs/plugins/transform-class-properties/]
Component Organization
- class definition
- constructor
- event handlers
- 'component' lifecycle events
- getters
- render
- constructor
- defaultProps
- proptypes
import React, {Componet,PropTypes } from 'react'
class Person extends Component {
static propTypes = {
name: PropTypes.string
}
static defaultProps = {
name: 'Guest'
}
constructor (props) {
super(props);
this.state = { smiling: false };
}
handleClick = () => {
this.setState({smiling: !this.state.smiling});
};
componentWillMount () {
// add event listeners (Store, WebSocket, document, etc.)
}
componentDidMount () {
// React.getDOMNode()
}
componentWillUnmount () {
// remove event listeners (Store, WebSocket, document, etc.)
}
get smilingMessage () {
return (this.state.smiling) ? "is smiling" : "";
}
render () {
return (
<div onClick={this.handleClick}>
{this.props.name} {this.smilingMessage}
</div>
);
}
}
Formatting Props
Wrap props on newlines for exactly 2 or more.
// bad
<Person
firstName="Michael" />
// good
<Person firstName="Michael" />
// bad
<Person firstName="Michael" lastName="Chan" occupation="Designer" favoriteFood="Drunken Noodles" />
// good
<Person
firstName="Michael"
lastName="Chan"
occupation="Designer"
favoriteFood="Drunken Noodles" />
Computed Props
Use getters to name computed properties.
// bad
firstAndLastName () {
return `${this.props.firstName} ${this.props.lastname}`;
}
// good
get fullName () {
return `${this.props.firstName} ${this.props.lastname}`;
}
See: Cached State in render anti-pattern
Compound State
Prefix compound state getters with a verb for readability.
// bad
happyAndKnowsIt () {
return this.state.happy && this.state.knowsIt;
}
// good
get isHappyAndKnowsIt () {
return this.state.happy && this.state.knowsIt;
}
These methods MUST return a boolean
value.
See: Compound Conditions anti-pattern
Prefer Ternary to Sub-render
Keep login inside the render
.
// bad
renderSmilingStatement () {
return <strong>{(this.state.isSmiling) ? " is smiling." : ""}</strong>;
},
render () {
return <div>{this.props.name}{this.renderSmilingStatement()}</div>;
}
// good
render () {
return (
<div>
{this.props.name}
{(this.state.smiling)
? <span>is smiling</span>
: null
}
</div>
);
}
View Components
Compose components into views. Don't create one-off components that merge layout and domain components.
// bad
class PeopleWrappedInBSRow extends React.Component {
render () {
return (
<div className="row">
<People people={this.state.people} />
</div>
);
}
}
// good
class BSRow extends React.Component {
render () {
return <div className="row">{this.props.children}</div>;
}
}
class SomeView extends React.Component {
render () {
return (
<BSRow>
<People people={this.state.people} />
</BSRow>
);
}
}
Container Components
A container does data fetching and then renders its corresponding sub-component. That's it. — Jason Bonta
Bad
// CommentList.js
class CommentList extends React.Component {
getInitialState () {
return { comments: [] };
}
componentDidMount () {
$.ajax({
url: "/my-comments.json",
dataType: 'json',
success: function(comments) {
this.setState({comments: comments});
}.bind(this)
});
}
render () {
return (
<ul>
{this.state.comments.map(({body, author}) => {
return <li>{body}—{author}</li>;
})}
</ul>
);
}
}
Good
// CommentList.js
class CommentList extends React.Component {
render() {
return (
<ul>
{this.props.comments.map(({body, author}) => {
return <li>{body}—{author}</li>;
})}
</ul>
);
}
}
// CommentListContainer.js
class CommentListContainer extends React.Component {
getInitialState () {
return { comments: [] }
}
componentDidMount () {
$.ajax({
url: "/my-comments.json",
dataType: 'json',
success: function(comments) {
this.setState({comments: comments});
}.bind(this)
});
}
render () {
return <CommentList comments={this.state.comments} />;
}
}
render
Cached State in Do not keep state in render
// bad
render () {
let name = `Mrs. ${this.props.name}`;
return <div>{name}</div>;
}
// good
render () {
return <div>{`Mrs. ${this.props.name}`}</div>;
}
// best
get fancyName () {
return `Mrs. ${this.props.name}`;
}
render () {
return <div>{this.fancyName}</div>;
}
This is mostly stylistic and keeps diffs nice. I doubt that there's a significant perf reason to do this.
See: Computed Props pattern
Compound Conditions
Don't put compound conditions in render
.
// bad
render () {
return <div>{if (this.state.happy && this.state.knowsIt) { return "Clapping hands" }</div>;
}
// better
get isTotesHappy() {
return this.state.happy && this.state.knowsIt;
},
render() {
return <div>{(this.isTotesHappy) && "Clapping hands"}</div>;
}
The best solution for this would use a container component to manage state and pass new state down as props.
See: Compound State pattern
Existence Checking
Do not check existence of props at the root of a component. Components should not have two possible return types.
// bad
const Person = props => {
if (this.props.firstName)
return <div>{this.props.firstName}</div>
else
return null
}
Components should always render. Consider adding defaultProps
, where a sensible default is appropriate.
// better
const Person = props =>
<div>{this.props.firstName}</div>
Person.defaultProps = {
firstName: "Guest"
}
If a component should be conditionally rendered, handle that in the owner component.
// best
const TheOwnerComponent = props =>
<div>
{props.person && <Person {...props.person} />}
</div>
This is only where objects or arrays are used. Use PropTypes.shape to clarify the types of nested data expected by the component.
Setting State from Props
Do not set state from props without obvious intent.
// bad
getInitialState () {
return {
items: this.props.items
};
}
// good
getInitialState () {
return {
items: this.props.initialItems
};
}
Read: "Props in getInitialState Is an Anti-Pattern"
Naming Handler Methods
Name the handler methods after their triggering event.
// bad
punchABadger () { /*...*/ },
render () {
return <div onClick={this.punchABadger} />;
}
// good
handleClick () { /*...*/ },
render () {
return <div onClick={this.handleClick} />;
}
Handler names should:
- begin with
handle
- end with the name of the event they handle (eg,
Click
,Change
) - be present-tense
If you need to disambiguate handlers, add additional information between
handle
and the event name. For example, you can distinguish between onChange
handlers: handleNameChange
and handleAgeChange
. When you do this, ask
yourself if you should be creating a new component.
Naming Events
Use custom event names for ownee events.
class Owner extends React.Component {
handleDelete () {
// handle Ownee's onDelete event
}
render () {
return <Ownee onDelete={this.handleDelete} />;
}
}
class Ownee extends React.Component {
render () {
return <div onChange={this.props.onDelete} />;
}
}
Ownee.propTypes = {
onDelete: React.PropTypes.func.isRequired
};
Using PropTypes
Use PropTypes to communicate expectations and log meaningful warnings.
MyValidatedComponent.propTypes = {
name: React.PropTypes.string
};
MyValidatedComponent
will log a warning if it receives name
of a type other than string
.
<Person name=1337 />
// Warning: Invalid prop `name` of type `number` supplied to `MyValidatedComponent`, expected `string`.
Components may also require props
.
MyValidatedComponent.propTypes = {
name: React.PropTypes.string.isRequired
}
This component will now validate the presence of name.
<Person />
// Warning: Required prop `name` was not specified in `Person`
Read: Prop Validation
Using Entities
Use React's String.fromCharCode()
for special characters.
// bad
<div>PiCO · Mascot</div>
// nope
<div>PiCO · Mascot</div>
// good
<div>{'PiCO ' + String.fromCharCode(183) + ' Mascot'}</div>
// better
<div>{`PiCO ${String.fromCharCode(183)} Mascot`}</div>
Read: JSX Gotchas
Tables
The browser thinks you're dumb. But React doesn't. Always use tbody
in
table
components.
// bad
render () {
return (
<table>
<tr>...</tr>
</table>
);
}
// good
render () {
return (
<table>
<tbody>
<tr>...</tr>
</tbody>
</table>
);
}
The browser is going to insert tbody
if you forget. React will continue to
insert new tr
s into the table
and confuse the heck out of you. Always use
tbody
.
classnames
Use classNames to manage conditional classes.
// bad
get classes () {
let classes = ['MyComponent'];
if (this.state.active) {
classes.push('MyComponent--active');
}
return classes.join(' ');
}
render () {
return <div className={this.classes} />;
}
// good
render () {
let classes = {
'MyComponent': true,
'MyComponent--active': this.state.active
};
return <div className={classnames(classes)} />;
}
Read: Class Name Manipulation