├── 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 |
240 | 241 |
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 | 620 | ... 621 |
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 | --------------------------------------------------------------------------------