├── LICENSE
└── README.md
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Ministry Centered Technologies
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | NO LONGER MAINTAINED
2 | ===
3 | Moved and updated at [reactpatterns.com](https://reactpatterns.com).
4 | Thanks for your help in developing this into the site.
5 |
6 | ---
7 |
8 | React
9 | =====
10 |
11 | *Mostly reasonable patterns for writing React on Rails*
12 |
13 | ## Table of Contents
14 |
15 | 1. [Scope](#scope)
16 | 1. Organization
17 | 1. [Component Organization](#component-organization)
18 | 1. [Formatting Props](#formatting-props)
19 | 1. Patterns
20 | 1. [Computed Props](#computed-props)
21 | 1. [Compound State](#compound-state)
22 | 1. [prefer-ternary-to-sub-render](#prefer-ternary-to-sub-render)
23 | 1. [View Components](#view-components)
24 | 1. [Container Components](#container-components)
25 | 1. Anti-patterns
26 | 1. [Compound Conditions](#compound-conditions)
27 | 1. [Cached State in render](#cached-state-in-render)
28 | 1. [Existence Checking](#existence-checking)
29 | 1. [Setting State from Props](#setting-state-from-props)
30 | 1. Practices
31 | 1. [Naming Handle Methods](#naming-handler-methods)
32 | 1. [Naming Events](#naming-events)
33 | 1. [Using PropTypes](#using-proptypes)
34 | 1. [Using Entities](#using-entities)
35 | 1. Gotchas
36 | 1. [Tables](#tables)
37 | 1. Libraries
38 | 1. [classnames](#classnames)
39 | 1. Other
40 | 1. [JSX](#jsx)
41 | 1. [ES2015](#es2015)
42 | 1. [react-rails](#react-rails)
43 | 1. [rails-assets](#rails-assets)
44 | 1. [flux](#flux)
45 |
46 | ---
47 |
48 | ## Scope
49 |
50 | This is how we write [React.js](https://facebook.github.io/react/) on Rails.
51 | We've struggled to find the happy path. Recommendations here represent a good
52 | number of failed attempts. If something seems out of place, it probably is;
53 | let us know what you've found.
54 |
55 | All examples written in ES2015 syntax now that the
56 | [official react-rails gem](https://github.com/reactjs/react-rails) ships with
57 | [babel](http://babeljs.io/).
58 |
59 | **[⬆ back to top](#table-of-contents)**
60 |
61 | ---
62 |
63 | ## Component Organization
64 |
65 | * class definition
66 | * constructor
67 | * event handlers
68 | * 'component' lifecycle events
69 | * getters
70 | * render
71 | * defaultProps
72 | * proptypes
73 |
74 | ```javascript
75 | class Person extends React.Component {
76 | constructor (props) {
77 | super(props);
78 |
79 | this.state = { smiling: false };
80 |
81 | this.handleClick = () => {
82 | this.setState({smiling: !this.state.smiling});
83 | };
84 | }
85 |
86 | componentWillMount () {
87 | // add event listeners (Flux Store, WebSocket, document, etc.)
88 | }
89 |
90 | componentDidMount () {
91 | // React.getDOMNode()
92 | }
93 |
94 | componentWillUnmount () {
95 | // remove event listeners (Flux Store, WebSocket, document, etc.)
96 | }
97 |
98 | get smilingMessage () {
99 | return (this.state.smiling) ? "is smiling" : "";
100 | }
101 |
102 | render () {
103 | return (
104 |
105 | {this.props.name} {this.smilingMessage}
106 |
107 | );
108 | }
109 | }
110 |
111 | Person.defaultProps = {
112 | name: 'Guest'
113 | };
114 |
115 | Person.propTypes = {
116 | name: React.PropTypes.string
117 | };
118 | ```
119 |
120 | **[⬆ back to top](#table-of-contents)**
121 |
122 | ## Formatting Props
123 |
124 | Wrap props on newlines for exactly 2 or more.
125 |
126 | ```html
127 | // bad
128 |
130 |
131 | // good
132 |
133 | ```
134 |
135 | ```html
136 | // bad
137 |
138 |
139 | // good
140 |
145 | ```
146 |
147 | **[⬆ back to top](#table-of-contents)**
148 |
149 | ---
150 |
151 | ## Computed Props
152 |
153 | Use getters to name computed properties.
154 |
155 | ```javascript
156 | // bad
157 | firstAndLastName () {
158 | return `${this.props.firstName} ${this.props.lastName}`;
159 | }
160 |
161 | // good
162 | get fullName () {
163 | return `${this.props.firstName} ${this.props.lastName}`;
164 | }
165 | ```
166 |
167 | See: [Cached State in render](#cached-state-in-render) anti-pattern
168 |
169 | **[⬆ back to top](#table-of-contents)**
170 |
171 | ---
172 |
173 | ## Compound State
174 |
175 | Prefix compound state getters with a verb for readability.
176 |
177 | ```javascript
178 | // bad
179 | happyAndKnowsIt () {
180 | return this.state.happy && this.state.knowsIt;
181 | }
182 | ```
183 |
184 | ```javascript
185 | // good
186 | get isHappyAndKnowsIt () {
187 | return this.state.happy && this.state.knowsIt;
188 | }
189 | ```
190 |
191 | These methods *MUST* return a `boolean` value.
192 |
193 | See: [Compound Conditions](#compound-conditions) anti-pattern
194 |
195 | **[⬆ back to top](#table-of-contents)**
196 |
197 | ## Prefer Ternary to Sub-render
198 |
199 | Keep logic inside the `render` function.
200 |
201 | ```javascript
202 | // bad
203 | renderSmilingStatement () {
204 | return {(this.state.isSmiling) ? " is smiling." : ""};
205 | },
206 |
207 | render () {
208 | return {this.props.name}{this.renderSmilingStatement()}
;
209 | }
210 | ```
211 |
212 | ```javascript
213 | // good
214 | render () {
215 | return (
216 |
217 | {this.props.name}
218 | {(this.state.smiling)
219 | ? is smiling
220 | : null
221 | }
222 |
223 | );
224 | }
225 | ```
226 |
227 | **[⬆ back to top](#table-of-contents)**
228 |
229 | ## View Components
230 |
231 | Compose components into views. Don't create one-off components that merge layout
232 | and domain components.
233 |
234 | ```javascript
235 | // bad
236 | class PeopleWrappedInBSRow extends React.Component {
237 | render () {
238 | return (
239 |
242 | );
243 | }
244 | }
245 | ```
246 |
247 | ```javascript
248 | // good
249 | class BSRow extends React.Component {
250 | render () {
251 | return {this.props.children}
;
252 | }
253 | }
254 |
255 | class SomeView extends React.Component {
256 | render () {
257 | return (
258 |
259 |
260 |
261 | );
262 | }
263 | }
264 | ```
265 |
266 | **[⬆ back to top](#table-of-contents)**
267 |
268 | ## Container Components
269 |
270 | > A container does data fetching and then renders its corresponding
271 | > sub-component. That's it. — Jason Bonta
272 |
273 | #### Bad
274 |
275 | ```javascript
276 | // CommentList.js
277 |
278 | class CommentList extends React.Component {
279 | getInitialState () {
280 | return { comments: [] };
281 | }
282 |
283 | componentDidMount () {
284 | $.ajax({
285 | url: "/my-comments.json",
286 | dataType: 'json',
287 | success: function(comments) {
288 | this.setState({comments: comments});
289 | }.bind(this)
290 | });
291 | }
292 |
293 | render () {
294 | return (
295 |
296 | {this.state.comments.map(({body, author}) => {
297 | return - {body}—{author}
;
298 | })}
299 |
300 | );
301 | }
302 | }
303 | ```
304 |
305 | #### Good
306 |
307 | ```javascript
308 | // CommentList.js
309 |
310 | class CommentList extends React.Component {
311 | render() {
312 | return (
313 |
314 | {this.props.comments.map(({body, author}) => {
315 | return - {body}—{author}
;
316 | })}
317 |
318 | );
319 | }
320 | }
321 | ```
322 |
323 | ```javascript
324 | // CommentListContainer.js
325 |
326 | class CommentListContainer extends React.Component {
327 | getInitialState () {
328 | return { comments: [] }
329 | }
330 |
331 | componentDidMount () {
332 | $.ajax({
333 | url: "/my-comments.json",
334 | dataType: 'json',
335 | success: function(comments) {
336 | this.setState({comments: comments});
337 | }.bind(this)
338 | });
339 | }
340 |
341 | render () {
342 | return ;
343 | }
344 | }
345 | ```
346 |
347 | [Read more](https://medium.com/@learnreact/container-components-c0e67432e005)
348 | [Watch more](https://www.youtube.com/watch?v=KYzlpRvWZ6c&t=1351)
349 |
350 | **[⬆ back to top](#table-of-contents)**
351 |
352 | ---
353 |
354 | ## Cached State in `render`
355 |
356 | Do not keep state in `render`
357 |
358 | ```javascript
359 | // bad
360 | render () {
361 | let name = `Mrs. ${this.props.name}`;
362 |
363 | return {name}
;
364 | }
365 |
366 | // good
367 | render () {
368 | return {`Mrs. ${this.props.name}`}
;
369 | }
370 | ```
371 |
372 | ```javascript
373 | // best
374 | get fancyName () {
375 | return `Mrs. ${this.props.name}`;
376 | }
377 |
378 | render () {
379 | return {this.fancyName}
;
380 | }
381 | ```
382 |
383 | *This is mostly stylistic and keeps diffs nice. I doubt that there's a significant perf reason to do this.*
384 |
385 | See: [Computed Props](#computed-props) pattern
386 |
387 | **[⬆ back to top](#table-of-contents)**
388 |
389 | ## Compound Conditions
390 |
391 | Don't put compound conditions in `render`.
392 |
393 | ```javascript
394 | // bad
395 | render () {
396 | return {if (this.state.happy && this.state.knowsIt) { return "Clapping hands" }
;
397 | }
398 | ```
399 |
400 | ```javascript
401 | // better
402 | get isTotesHappy() {
403 | return this.state.happy && this.state.knowsIt;
404 | },
405 |
406 | render() {
407 | return {(this.isTotesHappy) && "Clapping hands"}
;
408 | }
409 | ```
410 |
411 | The best solution for this would use a [container
412 | component](#container-components) to manage state and
413 | pass new state down as props.
414 |
415 | See: [Compound State](#compound-state) pattern
416 |
417 | **[⬆ back to top](#table-of-contents)**
418 |
419 | ## Existence Checking
420 |
421 | Do not check existence of props at the root of a component.
422 | Components should not have two possible return types.
423 |
424 | ```javascript
425 | // bad
426 | const Person = props => {
427 | if (this.props.firstName)
428 | return {this.props.firstName}
429 | else
430 | return null
431 | }
432 | ```
433 |
434 | Components should *always* render. Consider adding `defaultProps`, where a sensible default is appropriate.
435 |
436 | ```javascript
437 | // better
438 | const Person = props =>
439 | {this.props.firstName}
440 |
441 | Person.defaultProps = {
442 | firstName: "Guest"
443 | }
444 | ```
445 |
446 | If a component should be conditionally rendered, handle that in the owner component.
447 |
448 | ```javascript
449 | // best
450 | const TheOwnerComponent = props =>
451 |
452 | {props.person &&
}
453 |
454 | ```
455 |
456 | This is only where objects or arrays are used. Use PropTypes.shape to clarify
457 | the types of nested data expected by the component.
458 |
459 | **[⬆ back to top](#table-of-contents)**
460 |
461 | ## Setting State from Props
462 |
463 | Do not set state from props without obvious intent.
464 |
465 | ```javascript
466 | // bad
467 | getInitialState () {
468 | return {
469 | items: this.props.items
470 | };
471 | }
472 | ```
473 |
474 | ```javascript
475 | // good
476 | getInitialState () {
477 | return {
478 | items: this.props.initialItems
479 | };
480 | }
481 | ```
482 |
483 | Read: ["Props in getInitialState Is an Anti-Pattern"](http://facebook.github.io/react/tips/props-in-getInitialState-as-anti-pattern.html)
484 |
485 | **[⬆ back to top](#table-of-contents)**
486 |
487 | ---
488 |
489 | ## Naming Handler Methods
490 |
491 | Name the handler methods after their triggering event.
492 |
493 | ```javascript
494 | // bad
495 | punchABadger () { /*...*/ },
496 |
497 | render () {
498 | return ;
499 | }
500 | ```
501 |
502 | ```javascript
503 | // good
504 | handleClick () { /*...*/ },
505 |
506 | render () {
507 | return ;
508 | }
509 | ```
510 |
511 | Handler names should:
512 |
513 | - begin with `handle`
514 | - end with the name of the event they handle (eg, `Click`, `Change`)
515 | - be present-tense
516 |
517 | If you need to disambiguate handlers, add additional information between
518 | `handle` and the event name. For example, you can distinguish between `onChange`
519 | handlers: `handleNameChange` and `handleAgeChange`. When you do this, ask
520 | yourself if you should be creating a new component.
521 |
522 | **[⬆ back to top](#table-of-contents)**
523 |
524 | ## Naming Events
525 |
526 | Use custom event names for ownee events.
527 |
528 | ```javascript
529 | class Owner extends React.Component {
530 | handleDelete () {
531 | // handle Ownee's onDelete event
532 | }
533 |
534 | render () {
535 | return ;
536 | }
537 | }
538 |
539 | class Ownee extends React.Component {
540 | render () {
541 | return ;
542 | }
543 | }
544 |
545 | Ownee.propTypes = {
546 | onDelete: React.PropTypes.func.isRequired
547 | };
548 | ```
549 |
550 | **[⬆ back to top](#table-of-contents)**
551 |
552 | ## Using PropTypes
553 |
554 | Use PropTypes to communicate expectations and log meaningful warnings.
555 |
556 | ```javascript
557 | MyValidatedComponent.propTypes = {
558 | name: React.PropTypes.string
559 | };
560 | ```
561 | `MyValidatedComponent` will log a warning if it receives `name` of a type other than `string`.
562 |
563 |
564 | ```html
565 |
566 | // Warning: Invalid prop `name` of type `number` supplied to `MyValidatedComponent`, expected `string`.
567 | ```
568 |
569 | Components may also require `props`.
570 |
571 | ```javascript
572 | MyValidatedComponent.propTypes = {
573 | name: React.PropTypes.string.isRequired
574 | }
575 | ```
576 |
577 | This component will now validate the presence of name.
578 |
579 | ```html
580 |
581 | // Warning: Required prop `name` was not specified in `Person`
582 | ```
583 |
584 | Read: [Prop Validation](http://facebook.github.io/react/docs/reusable-components.html#prop-validation)
585 |
586 | **[⬆ back to top](#table-of-contents)**
587 |
588 | ## Using Entities
589 |
590 | Use React's `String.fromCharCode()` for special characters.
591 |
592 | ```javascript
593 | // bad
594 | PiCO · Mascot
595 |
596 | // nope
597 | PiCO · Mascot
598 |
599 | // good
600 | {'PiCO ' + String.fromCharCode(183) + ' Mascot'}
601 |
602 | // better
603 | {`PiCO ${String.fromCharCode(183)} Mascot`}
604 | ```
605 |
606 | Read: [JSX Gotchas](http://facebook.github.io/react/docs/jsx-gotchas.html#html-entities)
607 |
608 | **[⬆ back to top](#table-of-contents)**
609 |
610 | ## Tables
611 |
612 | The browser thinks you're dumb. But React doesn't. Always use `tbody` in
613 | `table` components.
614 |
615 | ```javascript
616 | // bad
617 | render () {
618 | return (
619 |
622 | );
623 | }
624 |
625 | // good
626 | render () {
627 | return (
628 |
629 |
630 | ...
631 |
632 |
633 | );
634 | }
635 | ```
636 |
637 | The browser is going to insert `tbody` if you forget. React will continue to
638 | insert new `tr`s into the `table` and confuse the heck out of you. Always use
639 | `tbody`.
640 |
641 | **[⬆ back to top](#table-of-contents)**
642 |
643 | ## classnames
644 |
645 | Use [classNames](https://www.npmjs.com/package/classnames) to manage conditional classes.
646 |
647 | ```javascript
648 | // bad
649 | get classes () {
650 | let classes = ['MyComponent'];
651 |
652 | if (this.state.active) {
653 | classes.push('MyComponent--active');
654 | }
655 |
656 | return classes.join(' ');
657 | }
658 |
659 | render () {
660 | return ;
661 | }
662 | ```
663 |
664 | ```javascript
665 | // good
666 | render () {
667 | let classes = {
668 | 'MyComponent': true,
669 | 'MyComponent--active': this.state.active
670 | };
671 |
672 | return ;
673 | }
674 | ```
675 |
676 | Read: [Class Name Manipulation](https://github.com/JedWatson/classnames/blob/master/README.md)
677 |
678 | **[⬆ back to top](#table-of-contents)**
679 |
680 | ## JSX
681 |
682 | We used to have some hardcore CoffeeScript lovers is the group. The unfortunate
683 | thing about writing templates in CoffeeScript is that it leaves you on the hook
684 | when certain implementations changes that JSX would normally abstract.
685 |
686 | We no longer recommend using CoffeeScript to write `render`.
687 |
688 | For posterity, you can read about how we used CoffeeScript, when using CoffeeScript was
689 | non-negotiable: [CoffeeScript and JSX](https://slack-files.com/T024L9M0Y-F02HP4JM3-80d714).
690 |
691 | **[⬆ back to top](#table-of-contents)**
692 |
693 | ## ES2015
694 |
695 | [react-rails](https://github.com/reactjs/react-rails) now ships with [babel](babeljs.io). Anything
696 | you can do in Babel, you can do in Rails. See the documentation for additional config.
697 |
698 | **[⬆ back to top](#table-of-contents)**
699 |
700 | ## react-rails
701 |
702 | [react-rails](https://github.com/reactjs/react-rails) should be used in all
703 | Rails apps that use React. It provides the perfect amount of glue between Rails
704 | conventions and React.
705 |
706 | **[⬆ back to top](#table-of-contents)**
707 |
708 | ## rails-assets
709 | [rails-assets](https://rails-assets.org) should be considered for bundling
710 | js/css assets into your applications. The most popular React-libraries we use
711 | are registered on [Bower](http://bower.io) and can be easily added through
712 | Bundler and react-assets.
713 |
714 | **caveats: rails-assets gives you access to bower projects via Sprockets
715 | requires. This is a win for the traditionally hand-wavy approach that Rails
716 | takes with JavaScript. This approach doesn't buy you modularity or the ability to
717 | interop with JS tooling that requires modules.**
718 |
719 | **[⬆ back to top](#table-of-contents)**
720 |
721 | ## flux
722 |
723 | Use [Alt](http://alt.js.org) for flux implementation. Alt is true to the flux
724 | pattern with the best documentation available.
725 |
726 | **[⬆ back to top](#table-of-contents)**
727 |
--------------------------------------------------------------------------------