Moved and updated at reactpatterns.com. Thanks for your help in developing this into the site.
Mostly reasonable patterns for writing React on Rails
- 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
- Other
- JSX
- ES2015
- react-rails
- rails-assets
- flux
This is how we write React.js on Rails. We've struggled to find the happy path. Recommendations here represent a good number of failed attempts. If something seems out of place, it probably is; let us know what you've found.
All examples written in ES2015 syntax now that the official react-rails gem ships with babel.
- class definition
- constructor
- event handlers
- 'component' lifecycle events
- getters
- render
- constructor
- defaultProps
- proptypes
classPersonextendsReact.Component{constructor(props){super(props);this.state={smiling: false};this.handleClick=()=>{this.setState({smiling: !this.state.smiling});};}componentWillMount(){// add event listeners (Flux Store, WebSocket, document, etc.)}componentDidMount(){// React.getDOMNode()}componentWillUnmount(){// remove event listeners (Flux Store, WebSocket, document, etc.)}getsmilingMessage(){return(this.state.smiling) ? "is smiling" : "";}render(){return(<divonClick={this.handleClick}>{this.props.name}{this.smilingMessage}</div>);}}Person.defaultProps={name: 'Guest'};Person.propTypes={name: React.PropTypes.string};Wrap props on newlines for exactly 2 or more.
// bad
<PersonfirstName="Michael" />
// good
<PersonfirstName="Michael" />// bad
<PersonfirstName="Michael" lastName="Chan" occupation="Designer" favoriteFood="Drunken Noodles" />
// good
<PersonfirstName="Michael"
lastName="Chan"
occupation="Designer"
favoriteFood="Drunken Noodles" />Use getters to name computed properties.
// badfirstAndLastName(){return`${this.props.firstName}${this.props.lastName}`;}// goodgetfullName(){return`${this.props.firstName}${this.props.lastName}`;}See: Cached State in render anti-pattern
Prefix compound state getters with a verb for readability.
// badhappyAndKnowsIt(){returnthis.state.happy&&this.state.knowsIt;}// goodgetisHappyAndKnowsIt(){returnthis.state.happy&&this.state.knowsIt;}These methods MUST return a boolean value.
See: Compound Conditions anti-pattern
Keep logic inside the render function.
// badrenderSmilingStatement(){return<strong>{(this.state.isSmiling) ? " is smiling." : ""}</strong>;},render(){return<div>{this.props.name}{this.renderSmilingStatement()}</div>;}// goodrender(){return(<div>{this.props.name}{(this.state.smiling)
? <span>is smiling</span>
: null}</div>);}Compose components into views. Don't create one-off components that merge layout and domain components.
// badclassPeopleWrappedInBSRowextendsReact.Component{render(){return(<divclassName="row"><Peoplepeople={this.state.people}/></div>);}}// goodclassBSRowextendsReact.Component{render(){return<divclassName="row">{this.props.children}</div>;}}classSomeViewextendsReact.Component{render(){return(<BSRow><Peoplepeople={this.state.people}/></BSRow>);}}A container does data fetching and then renders its corresponding sub-component. That's it. — Jason Bonta
// CommentList.jsclassCommentListextendsReact.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>);}}// CommentList.jsclassCommentListextendsReact.Component{render(){return(<ul>{this.props.comments.map(({body, author})=>{return<li>{body}—{author}</li>;})}</ul>);}}// CommentListContainer.jsclassCommentListContainerextendsReact.Component{getInitialState(){return{comments: []}}componentDidMount(){$.ajax({url: "/my-comments.json",dataType: 'json',success: function(comments){this.setState({comments: comments});}.bind(this)});}render(){return<CommentListcomments={this.state.comments}/>;}}Do not keep state in render
// badrender(){letname=`Mrs. ${this.props.name}`;return<div>{name}</div>;}// goodrender(){return<div>{`Mrs. ${this.props.name}`}</div>;}// bestgetfancyName(){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
Don't put compound conditions in render.
// badrender(){return<div>{if(this.state.happy&&this.state.knowsIt){return"Clapping hands"}</div>;}// bettergetisTotesHappy(){returnthis.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
Do not check existence of props at the root of a component. Components should not have two possible return types.
// badconstPerson=props=>{if(this.props.firstName)return<div>{this.props.firstName}</div>elsereturnnull}Components should always render. Consider adding defaultProps, where a sensible default is appropriate.
// betterconstPerson=props=><div>{this.props.firstName}</div>Person.defaultProps={firstName: "Guest"}If a component should be conditionally rendered, handle that in the owner component.
// bestconstTheOwnerComponent=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.
Do not set state from props without obvious intent.
// badgetInitialState(){return{items: this.props.items};}// goodgetInitialState(){return{items: this.props.initialItems};}Read: "Props in getInitialState Is an Anti-Pattern"
Name the handler methods after their triggering event.
// badpunchABadger(){/*...*/},render(){return<divonClick={this.punchABadger}/>;}// goodhandleClick(){/*...*/},render(){return<divonClick={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.
Use custom event names for ownee events.
classOwnerextendsReact.Component{handleDelete(){// handle Ownee's onDelete event}render(){return<OwneeonDelete={this.handleDelete}/>;}}classOwneeextendsReact.Component{render(){return<divonChange={this.props.onDelete}/>;}}Ownee.propTypes={onDelete: React.PropTypes.func.isRequired};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.
<Personname=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
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
The browser thinks you're dumb. But React doesn't. Always use tbody in
table components.
// badrender(){return(<table><tr>...</tr></table>);}// goodrender(){return(<table><tbody><tr>...</tr></tbody></table>);}The browser is going to insert tbody if you forget. React will continue to
insert new trs into the table and confuse the heck out of you. Always use
tbody.
Use classNames to manage conditional classes.
// badgetclasses(){letclasses=['MyComponent'];if(this.state.active){classes.push('MyComponent--active');}returnclasses.join(' ');}render(){return<divclassName={this.classes}/>;}// goodrender(){letclasses={'MyComponent': true,'MyComponent--active': this.state.active};return<divclassName={classnames(classes)}/>;}Read: Class Name Manipulation
We used to have some hardcore CoffeeScript lovers is the group. The unfortunate thing about writing templates in CoffeeScript is that it leaves you on the hook when certain implementations changes that JSX would normally abstract.
We no longer recommend using CoffeeScript to write render.
For posterity, you can read about how we used CoffeeScript, when using CoffeeScript was non-negotiable: CoffeeScript and JSX.
react-rails now ships with babel. Anything you can do in Babel, you can do in Rails. See the documentation for additional config.
react-rails should be used in all Rails apps that use React. It provides the perfect amount of glue between Rails conventions and React.
rails-assets should be considered for bundling js/css assets into your applications. The most popular React-libraries we use are registered on Bower and can be easily added through Bundler and react-assets.
caveats: rails-assets gives you access to bower projects via Sprockets requires. This is a win for the traditionally hand-wavy approach that Rails takes with JavaScript. This approach doesn't buy you modularity or the ability to interop with JS tooling that requires modules.
Use Alt for flux implementation. Alt is true to the flux pattern with the best documentation available.