React Mostly reasonable patterns for writing React on Rails
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
Other
JSX
ES2015
react-rails
rails-assets
flux
Scope 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 theofficial react-rails gem ships withbabel .
⬆ back to top
Component Organization
class definition
constructor
‘component’ lifecycle events
getters
render
defaultProps
proptypes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class Person extends React .Component { constructor (props) { super (props); this .state = { smiling : false }; this .handleClick = () => { this .setState({smiling : !this .state.smiling}); }; } componentWillMount () { } componentDidMount () { } componentWillUnmount () { } get smilingMessage () { return (this .state.smiling) ? "is smiling" : "" ; } render () { return ( <div onClick={this .handleClick}> {this .props.name} {this .smilingMessage} </div> ); } } Person.defaultProps = { name: 'Guest' }; Person.propTypes = { name: React.PropTypes.string };
⬆ back to top
Wrap props on newlines for exactly 2 or more.
1 2 3 4 5 6 // bad <Person firstName ="Michael" /> // good <Person firstName ="Michael" />
1 2 3 4 5 6 7 8 9 // bad <Person firstName ="Michael" lastName ="Chan" occupation ="Designer" favoriteFood ="Drunken Noodles" /> // good <Person firstName ="Michael" lastName ="Chan" occupation ="Designer" favoriteFood ="Drunken Noodles" />
⬆ back to top
Computed Props Use getters to name computed properties.
1 2 3 4 5 6 7 8 9 firstAndLastName () { return `${this .props.firstName} ${this .props.lastName} ` ; } get fullName () { return `${this .props.firstName} ${this .props.lastName} ` ; }
See: Cached State in render anti-pattern
⬆ back to top
Compound State Prefix compound state getters with a verb for readability.
1 2 3 4 happyAndKnowsIt () { return this .state.happy && this .state.knowsIt; }
1 2 3 4 get isHappyAndKnowsIt () { return this .state.happy && this .state.knowsIt; }
These methods MUST return a boolean value.
See: Compound Conditions anti-pattern
⬆ back to top
Prefer Ternary to Sub-render Keep logic inside the render function.
1 2 3 4 5 6 7 8 renderSmilingStatement () { return <strong > {(this.state.isSmiling) ? " is smiling." : ""}</strong > ; }, render () { return <div > {this.props.name}{this.renderSmilingStatement()}</div > ; }
1 2 3 4 5 6 7 8 9 10 11 12 render () { return ( <div> {this .props.name} {(this .state.smiling) ? <span > is smiling</span > : null } </div> ); }
⬆ back to top
View Components Compose components into views. Don’t create one-off components that merge layout and domain components.
1 2 3 4 5 6 7 8 9 10 class PeopleWrappedInBSRow extends React .Component { render () { return ( <div className="row" > <People people={this .state.people} /> </div> ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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> ); } }
⬆ back to top
Container Components
A container does data fetching and then renders its corresponding sub-component. That’s it. — Jason Bonta
Bad 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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 1 2 3 4 5 6 7 8 9 10 11 12 13 class CommentList extends React .Component { render() { return ( <ul> {this .props.comments.map(({body, author} ) => { return <li > {body}—{author}</li > ; })} </ul> ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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} /> ; } }
Read more Watch more
⬆ back to top
Cached State in render Do not keep state in render
1 2 3 4 5 6 7 8 9 10 11 render () { let name = `Mrs. ${this .props.name} ` ; return <div > {name}</div > ; } render () { return <div > {`Mrs. ${this.props.name}`}</div > ; }
1 2 3 4 5 6 7 8 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
⬆ back to top
Compound Conditions Don’t put compound conditions in render.
1 2 3 4 render () { return <div > {if (this.state.happy && this.state.knowsIt) { return "Clapping hands" }</div > ; }
1 2 3 4 5 6 7 8 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
⬆ back to top
Existence Checking Do not check existence of props at the root of a component. Components should not have two possible return types.
1 2 3 4 5 6 7 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.
1 2 3 4 5 6 7 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.
1 2 3 4 5 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.
⬆ back to top
Setting State from Props Do not set state from props without obvious intent.
1 2 3 4 5 6 getInitialState () { return { items: this .props.items }; }
1 2 3 4 5 6 getInitialState () { return { items: this .props.initialItems }; }
Read: “Props in getInitialState Is an Anti-Pattern”
⬆ back to top
Naming Handler Methods Name the handler methods after their triggering event.
1 2 3 4 5 6 punchABadger () { }, render () { return <div onClick ={this.punchABadger} /> ; }
1 2 3 4 5 6 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 betweenhandle 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.
⬆ back to top
Naming Events Use custom event names for ownee events.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Owner extends React .Component { handleDelete () { } 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 };
⬆ back to top
Using PropTypes Use PropTypes to communicate expectations and log meaningful warnings.
1 2 3 MyValidatedComponent.propTypes = { name: React.PropTypes.string };
MyValidatedComponent will log a warning if it receives name of a type other than string.
1 2 <Person name =1337 /> // Warning: Invalid prop `name` of type `number` supplied to `MyValidatedComponent`, expected `string`.
Components may also require props.
1 2 3 MyValidatedComponent.propTypes = { name: React.PropTypes.string.isRequired }
This component will now validate the presence of name.
1 2 <Person /> // Warning: Required prop `name` was not specified in `Person`
Read: Prop Validation
⬆ back to top
Using Entities Use React’s String.fromCharCode() for special characters.
1 2 3 4 5 6 7 8 9 10 11 <div>PiCO · Mascot</div> / / nope <div>PiCO · Mascot</ div><div>{'PiCO ' + String .fromCharCode(183 ) + ' Mascot' }</div> / / better <div>{`PiCO ${String.fromCharCode(183)} Mascot`}</ div>
Read: JSX Gotchas
⬆ back to top
Tables The browser thinks you’re dumb. But React doesn’t. Always use tbody intable components.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 render () { return ( <table> <tr>...</tr> </ table> ); } render () { 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 usetbody.
⬆ back to top
classnames Use classNames to manage conditional classes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 get classes () { let classes = ['MyComponent' ]; if (this .state.active) { classes.push('MyComponent--active' ); } return classes.join(' ' ); } render () { return <div className ={this.classes} /> ; }
1 2 3 4 5 6 7 8 9 render () { let classes = { 'MyComponent' : true , 'MyComponent--active' : this .state.active }; return <div className ={classnames(classes)} /> ; }
Read: Class Name Manipulation
⬆ back to top
JSX 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 .
⬆ back to top
ES2015 react-rails now ships with babel . Anything you can do in Babel, you can do in Rails. See the documentation for additional config.
⬆ back to top
react-rails react-rails should be used in all Rails apps that use React. It provides the perfect amount of glue between Rails conventions and React.
⬆ back to top
rails-assets 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.
⬆ back to top
flux Use Alt for flux implementation. Alt is true to the flux pattern with the best documentation available.
⬆ back to top