├── 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 |
--------------------------------------------------------------------------------