├── LICENSE └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 shimo 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 | 2 | React Cookbook 3 | ===== 4 | 5 | *编写简洁漂亮,可维护的 React 应用* 6 | 7 | ## 目录 8 | - [前言](#前言) 9 | - [组件声明](#组件声明) 10 | - [计算属性](#计算属性) 11 | - [事件回调命名](#事件回调命名) 12 | - [组件化优于多层 render](#组件化优于多层-render) 13 | - [状态上移优于公共方法](#状态上移优于公共方法) 14 | - [容器组件](#容器组件) 15 | - [纯函数的 render](#纯函数的-render) 16 | - [始终声明 PropTypes](#始终声明-proptypes) 17 | - [Props 非空检测](#props-非空检测) 18 | - [使用 Props 初始化](#使用-props-初始化) 19 | - [classnames](#classnames) 20 | 21 | --- 22 | 23 | ## 前言 24 | 25 | 随着应用规模和维护人数的增加,光靠 React 本身灵活易用的 API 并不足以有效控制应用的复杂度。本指南旨在在 ESLint 之外,再建立一个我们团队内较为一致认可的约定,以增加代码一致性和可读性、降低维护成本。 26 | 27 | _欢迎在 [Issues](https://github.com/shimohq/react-cookbook/issues) 进行相关讨论_ 28 | 29 | ## 组件声明 30 | 31 | 全面使用 ES6 class 声明,可不严格遵守该属性声明次序,但如有 propTypes 则必须写在顶部, lifecycle events 必须写到一起。 32 | 33 | * class 34 | * propTypes 35 | * defaultPropTypes 36 | * constructor 37 | * event handlers (如不使用[类属性](http://babeljs.io/docs/plugins/transform-class-properties/)语法可在此声明) 38 | * lifecycle events 39 | * event handlers 40 | * getters 41 | * render 42 | 43 | ```javascript 44 | class Person extends React.Component { 45 | static propTypes = { 46 | firstName: PropTypes.string.isRequired, 47 | lastName: PropTypes.string.isRequired 48 | } 49 | constructor (props) { 50 | super(props) 51 | 52 | this.state = { smiling: false } 53 | 54 | /* 若不能使用 babel-plugin-transform-class-properties 55 | this.handleClick = () => { 56 | this.setState({smiling: !this.state.smiling}) 57 | } 58 | */ 59 | } 60 | 61 | componentWillMount () {} 62 | 63 | componentDidMount () {} 64 | 65 | // ... 66 | 67 | handleClick = () => { 68 | this.setState({smiling: !this.state.smiling}) 69 | } 70 | 71 | get fullName () { 72 | return this.props.firstName + this.props.lastName 73 | } 74 | 75 | render () { 76 | return ( 77 |
78 | {this.fullName} {this.state.smiling ? 'is smiling.' : ''} 79 |
80 | ) 81 | } 82 | } 83 | ``` 84 | 85 | **[⬆ 回到目录](#目录)** 86 | 87 | ## 计算属性 88 | 89 | 使用 getters 封装 render 所需要的状态或条件的组合 90 | 91 | 对于返回 boolean 的 getter 使用 is- 前缀命名 92 | 93 | ```javascript 94 | // bad 95 | render () { 96 | return ( 97 |
98 | { 99 | this.state.age > 18 100 | && (this.props.school === 'A' 101 | || this.props.school === 'B') 102 | ? 103 | : 104 | } 105 |
106 | ) 107 | } 108 | 109 | // good 110 | get isVIP() { 111 | return 112 | this.state.age > 18 113 | && (this.props.school === 'A' 114 | || this.props.school === 'B') 115 | } 116 | render() { 117 | return ( 118 |
119 | {this.isVIP ? : } 120 |
121 | ) 122 | } 123 | ``` 124 | 125 | **[⬆ 回到目录](#目录)** 126 | 127 | ## 事件回调命名 128 | 129 | Handler 命名风格: 130 | 131 | - 使用 `handle` 开头 132 | - 以事件类型作为结尾 (如 `Click`, `Change`) 133 | - 使用一般现在时 134 | 135 | ```javascript 136 | // bad 137 | closeAll = () => {}, 138 | 139 | render () { 140 | return
141 | } 142 | ``` 143 | 144 | ```javascript 145 | // good 146 | handleClick = () => {}, 147 | 148 | render () { 149 | return
150 | } 151 | ``` 152 | 153 | 如果你需要区分同样事件类型的 handler(如 `handleNameChange` 和 `handleEmailChange`)时,可能这就是一个拆分组件的信号 154 | 155 | **[⬆ 回到目录](#目录)** 156 | 157 | ## 组件化优于多层 render 158 | 159 | 当组件的 jsx 只写在一个 render 方法显得太臃肿时,很可能更适合拆分出一个组件,视情况采用 class component 或 stateless component 160 | 161 | ```javascript 162 | // bad 163 | renderItem ({name}) { 164 | return ( 165 |
  • 166 | {name} 167 | {/* ... */} 168 |
  • 169 | ) 170 | } 171 | 172 | render () { 173 | return ( 174 |
    175 |
      176 | {this.props.items.map(item => this.renderItem(item))} 177 |
    178 |
    179 | ) 180 | } 181 | ``` 182 | 183 | ```javascript 184 | // good 185 | function Items ({name}) { 186 | return ( 187 |
  • 188 | {name} 189 | {/* ... */} 190 |
  • 191 | ) 192 | } 193 | 194 | render () { 195 | return ( 196 |
    197 |
      198 | {this.props.items.map(item => )} 199 |
    200 |
    201 | ) 202 | } 203 | ``` 204 | 205 | **[⬆ 回到目录](#目录)** 206 | 207 | ## 状态上移优于公共方法 208 | 209 | 一般组件不应提供公共方法,这样会破坏数据流只有一个方向的原则。 210 | 211 | 再因为我们倾向于更细颗粒的组件化,状态应集中在远离渲染的地方处理(比如应用级别的状态就在 redux 的 store 里),也能使兄弟组件更方便地共享。 212 | 213 | ```javascript 214 | //bad 215 | class DropDownMenu extends Component { 216 | constructor (props) { 217 | super(props) 218 | this.state = { 219 | showMenu: false 220 | } 221 | } 222 | 223 | show () { 224 | this.setState({display: true}) 225 | } 226 | 227 | hide () { 228 | this.setState({display: false}) 229 | } 230 | 231 | render () { 232 | return this.state.display && ( 233 |
    234 | {/* ... */} 235 |
    236 | ) 237 | } 238 | } 239 | 240 | class MyComponent extends Component { 241 | // ... 242 | showMenu () { 243 | this.refs.menu.show() 244 | } 245 | hideMenu () { 246 | this.refs.menu.hide() 247 | } 248 | render () { 249 | return 250 | } 251 | } 252 | 253 | //good 254 | class DropDownMenu extends Component { 255 | static propsType = { 256 | display: PropTypes.boolean.isRequired 257 | } 258 | 259 | render () { 260 | return this.props.display && ( 261 |
    262 | {/* ... */} 263 |
    264 | ) 265 | } 266 | } 267 | 268 | class MyComponent extends Component { 269 | constructor (props) { 270 | super(props) 271 | this.state = { 272 | showMenu: false 273 | } 274 | } 275 | 276 | // ... 277 | 278 | showMenu () { 279 | this.setState({showMenu: true}) 280 | } 281 | 282 | hideMenu () { 283 | this.setState({showMenu: false}) 284 | } 285 | 286 | render () { 287 | return 288 | } 289 | } 290 | ``` 291 | 292 | 更多阅读: [lifting-state-up](https://facebook.github.io/react/docs/lifting-state-up.html) 293 | 294 | ## 容器组件 295 | 296 | 一个容器组件主要负责维护状态和数据的计算,本身并没有界面逻辑,只把结果通过 props 传递下去。 297 | 298 | 区分容器组件的目的就是可以把组件的状态和渲染解耦开来,改写界面时可不用关注数据的实现,顺便得到了可复用性。 299 | 300 | ```javascript 301 | // bad 302 | class MessageList extends Component { 303 | constructor (props) { 304 | super(props) 305 | this.state = { 306 | onlyUnread: false, 307 | messages: [] 308 | } 309 | } 310 | 311 | componentDidMount () { 312 | $.ajax({ 313 | url: "/api/messages", 314 | }).then(({messages}) => this.setState({messages})) 315 | } 316 | 317 | handleClick = () => this.setState({onlyUnread: !this.state.onlyUnread}) 318 | 319 | render () { 320 | return ( 321 |
    322 |
      323 | { 324 | this.state.messages 325 | .filter(msg => this.state.onlyUnread ? !msg.asRead : true) 326 | .map(({content, author}) => { 327 | return
    • {content}—{author}
    • 328 | }) 329 | } 330 |
    331 | 332 |
    333 | ) 334 | } 335 | } 336 | ``` 337 | 338 | ```javascript 339 | // good 340 | class MessageContainer extends Component { 341 | constructor (props) { 342 | super(props) 343 | this.state = { 344 | onlyUnread: false, 345 | messages: [] 346 | } 347 | } 348 | 349 | componentDidMount () { 350 | $.ajax({ 351 | url: "/api/messages", 352 | }).then(({messages}) => this.setState({messages})) 353 | } 354 | 355 | handleClick = () => this.setState({onlyUnread: !this.state.onlyUnread}) 356 | 357 | render () { 358 | return this.state.onlyUnread ? !msg.asRead : true)} 360 | toggleUnread={this.handleClick} 361 | /> 362 | } 363 | } 364 | 365 | function MessageList ({messages, toggleUnread}) { 366 | return ( 367 |
    368 |
      369 | { 370 | messages 371 | .map(({content, author}) => { 372 | return
    • {content}—{author}
    • 373 | }) 374 | } 375 |
    376 | 377 |
    378 | ) 379 | } 380 | MessageList.propTypes = { 381 | messages: propTypes.array.isRequired, 382 | toggleUnread: propTypes.func.isRequired 383 | } 384 | ``` 385 | 386 | 更多阅读: 387 | - [Presentational and Container Components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.sz7z538t6) 388 | - [React AJAX Best Practices](http://andrewhfarmer.com/react-ajax-best-practices/) 389 | 390 | **[⬆ 回到目录](#目录)** 391 | 392 | ## 纯函数的 render 393 | 394 | render 函数应该是一个纯函数(stateless component 当然也是),不依赖 this.state、this.props 以外的变量,也不改变外部状态 395 | 396 | ```javascript 397 | // bad 398 | render () { 399 | return
    {window.navigator.userAgent}
    400 | } 401 | 402 | // good 403 | render () { 404 | return
    {this.props.userAgent}
    405 | } 406 | ``` 407 | 408 | 更多阅读: [Return as soon as you know the answer](https://medium.com/@SimonRadionov/return-as-soon-as-you-know-the-answer-dec6369b9b67#.q67w8z60g) 409 | 410 | **[⬆ 回到目录](#目录)** 411 | 412 | ## 始终声明 PropTypes 413 | 414 | 每一个组件都声明 PropTypes,非必须的 props 应提供默认值。 415 | 416 | 对于非常广为人知的 props 如 children, dispatch 也不应该忽略。因为如果一个组件没有声明 dispatch 的 props,那么一眼就可以知道该组件没有修改 store 了。 417 | 418 | 但如果在开发一系列会 dispatch 的组件时,可在这些组件的目录建立单独的 .eslintrc 来只忽略 dispatch。 419 | 420 | 更多阅读: [Prop Validation](http://facebook.github.io/react/docs/reusable-components.html#prop-validation) 421 | 422 | **[⬆ 回到目录](#目录)** 423 | 424 | ## Props 非空检测 425 | 426 | 对于并非 `isRequired` 的 proptype,必须对应设置 defaultProps,避免再增加 if 分支带来的负担 427 | 428 | ```javascript 429 | // bad 430 | render () { 431 | if (this.props.person) { 432 | return
    {this.props.person.firstName}
    433 | } else { 434 | return
    Guest
    435 | } 436 | } 437 | ``` 438 | 439 | ```javascript 440 | // good 441 | class MyComponent extends Component { 442 | render() { 443 | return
    {this.props.person.firstName}
    444 | } 445 | } 446 | 447 | MyComponent.defaultProps = { 448 | person: { 449 | firstName: 'Guest' 450 | } 451 | } 452 | ``` 453 | 454 | 如有必要,使用 PropTypes.shape 明确指定需要的属性 455 | 456 | **[⬆ 回到目录](#目录)** 457 | 458 | ## 使用 Props 初始化 459 | 460 | 除非 props 的命名明确指出了意图,否则不该使用 props 来初始化 state 461 | 462 | ```javascript 463 | // bad 464 | constructor (props) { 465 | this.state = { 466 | items: props.items 467 | } 468 | } 469 | ``` 470 | 471 | ```javascript 472 | // good 473 | constructor (props) { 474 | this.state = { 475 | items: props.initialItems 476 | } 477 | } 478 | ``` 479 | 480 | 更多阅读: ["Props in getInitialState Is an Anti-Pattern"](http://facebook.github.io/react/tips/props-in-getInitialState-as-anti-pattern.html) 481 | 482 | **[⬆ 回到目录](#目录)** 483 | 484 | ## classnames 485 | 486 | 使用 [classNames](https://www.npmjs.com/package/classnames) 来组合条件结果. 487 | 488 | ```javascript 489 | // bad 490 | render () { 491 | return
    492 | } 493 | ``` 494 | 495 | ```javascript 496 | // good 497 | render () { 498 | const classes = { 499 | menu: true, 500 | active: this.props.display 501 | } 502 | 503 | return
    504 | } 505 | ``` 506 | 507 | Read: [Class Name Manipulation](https://github.com/JedWatson/classnames/blob/master/README.md) 508 | 509 | **[⬆ 回到目录](#目录)** 510 | --------------------------------------------------------------------------------