我最开始学习react的时候,看到过各种各样编写组件的方式,不同教程中提出的方法往往有很大不同。当时虽说react这个框架已经十分成熟,但是似乎还没有一种公认正确的使用方法。过去几年中,我们团队编写了很多react组件,我们对实现方法进行了不断的优化,直到满意。
本文介绍了我们在实践中的***实践方式,希望能对无论是初学者还是有经验的开发者来说都有一定的帮助。
在我们开始之前,有几点需要说明:
我们是用es6和es7语法
如果你不了解展示组件和容器组件的区别,可以先阅读这篇文章
如果你有任何建议、问题或者反馈,可以给我们留言
Class Based Components (基于类的组件)
Class based components 有自己的state和方法。我们会尽可能谨慎的使用这些组件,但是他们有自己的使用场景。
接下来我们就一行一行来编写组件。
导入CSS
复制
import React, { Component } from 'react' import { observer } from 'mobx-react' import ExpandableForm from './ExpandableForm' import './styles/ProfileContainer.css'
1.
2.
3.
4.
5.
6.
7.
我很喜欢CSS in JS,但是它目前还是一种新的思想,成熟的解决方案还未产生。我们在每个组件中都导入了它的css文件。
译者注:目前CSS in JS可以使用css modules方案来解决,webpack的css-loader已经提供了该功能
我们还用一个空行来区分自己的依赖。
译者注:即第4、5行和第1、2行中间会单独加行空行。
初始化state
复制
import React, { Component } from 'react' import { observer } from 'mobx-react' import ExpandableForm from './ExpandableForm' import './styles/ProfileContainer.css' export default class ProfileContainer extends Component { state = { expanded: false }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
你也可以在constructor中初始化state,不过我们更喜欢这种简洁的方式。我们还会确保默认导出组件的class。
propTypes 和 defaultProps
复制
import React, { Component } from 'react' import { observer } from 'mobx-react' import { string, object } from 'prop-types' import ExpandableForm from './ExpandableForm' import './styles/ProfileContainer.css' export default class ProfileContainer extends Component { state = { expanded: false } static propTypes = { model: object.isRequired, title: string } static defaultProps = { model: { id: 0 }, title: 'Your Name' }
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.
propTypes和defaultProps是静态属性,应该尽可能在代码的顶部声明。这两个属性起着文档的作用,应该能够使阅读代码的开发者一眼就能够看到。如果你正在使用react 15.3.0或者更高的版本,使用prop-types,而不是React.PropTypes。你的所有组件,都应该有propTypes属性。
方法
复制
import React, { Component } from 'react' import { observer } from 'mobx-react' import { string, object } from 'prop-types' import ExpandableForm from './ExpandableForm' import './styles/ProfileContainer.css' export default class ProfileContainer extends Component { state = { expanded: false } static propTypes = { model: object.isRequired, title: string } static defaultProps = { model: { id: 0 }, title: 'Your Name' } handleSubmit = (e) => { e.preventDefault() this.props.model.save() } handleNameChange = (e) => { this.props.model.changeName(e.target.value) } handleExpand = (e) => { e.preventDefault() this.setState({ expanded: !this.state.expanded }) }
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.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
使用class components,当你向子组件传递方法的时候,需要确保这些方法被调用时有正确的this值。通常会在向子组件传递时使用this.handleSubmit.bind(this)来实现。当然,使用es6的箭头函数写法更加简洁。
译者注:也可以在constructor中完成方法的上下文的绑定:
复制
constructor() { this.handleSubmit = this.handleSubmit.bind(this); }
1.
2.
3.
4.
5.
给setState传入一个函数作为参数(passing setState a Function)
在上文的例子中,我们是这么做的:
复制
this.setState({ expanded: !this.state.expanded })
1.
setState实际是异步执行的,react因为性能原因会将state的变化整合,再一起处理,因此当setState被调用的时候,state并不一定会立即变化。
这意味着在调用setState的时候你不能依赖当前的state值——因为你不能确保setState真正被调用的时候state究竟是什么。
解决方案就是给setState传入一个方法,该方法接收上一次的state作为参数。
this.setState(prevState => ({ expanded: !prevState.expanded })
解构props
复制
import React, { Component } from 'react' import { observer } from 'mobx-react' import { string, object } from 'prop-types' import ExpandableForm from './ExpandableForm' import './styles/ProfileContainer.css' export default class ProfileContainer extends Component { state = { expanded: false } static propTypes = { model: object.isRequired, title: string } static defaultProps = { model: { id: 0 }, title: 'Your Name' } handleSubmit = (e) => { e.preventDefault() this.props.model.save() } handleNameChange = (e) => { this.props.model.changeName(e.target.value) } handleExpand = (e) => { e.preventDefault() this.setState(prevState => ({ expanded: !prevState.expanded })) } render() { const { model, title } = this.props return ( <ExpandableForm onSubmit={this.handleSubmit} expanded={this.state.expanded} onExpand={this.handleExpand}> <div> <h1>{title}</h1> <input type="text" value={model.name} onChange={this.handleNameChange} placeholder="Your Name"/> </div> </ExpandableForm> ) } }
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.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
对于有很多props的组件来说,应当像上述写法一样,将每个属性解构出来,且每个属性单独一行。
装饰器(Decorators)
复制
@observer export default class ProfileContainer extends Component {
1.
2.
3.
如果你正在使用类似于mobx的状态管理器,你可以按照上述方式描述你的组件。这种写法与将组件作为参数传递给一个函数效果是一样的。装饰器(decorators)是一种非常灵活和易读的定义组件功能的方式。我们使用mobx和mobx-models来结合装饰器进行使用。
如果你不想使用装饰器,可以按照如下方式来做:
复制
class ProfileContainer extends Component { // Component code } export default observer(ProfileContainer)
1.
2.
3.
4.
5.
6.
7.
闭包
避免向子组件传入闭包,如下:
复制
<input type="text" value={model.name} // onChange={(e) => { model.name = e.target.value }} // ^ 不要这样写,按如下写法: onChange={this.handleChange} placeholder="Your Name"/>
1.
2.
3.
4.
5.
6.
7.
原因在于:每次父组件重新渲染时,都会创建一个新的函数,并传给input。
如果这个input是个react组件的话,这会导致无论该组件的其他属性是否变化,该组件都会重新render。
而且,采用将父组件的方法传入的方式也会使得代码更易读,方便调试,同时也容易修改。
完整代码如下:
复制
import React, { Component } from 'react' import { observer } from 'mobx-react' import { string, object } from 'prop-types' // Separate local imports from dependencies import ExpandableForm from './ExpandableForm' import './styles/ProfileContainer.css' // Use decorators if needed @observer export default class ProfileContainer extends Component { state = { expanded: false } // Initialize state here (ES7) or in a constructor method (ES6) // Declare propTypes as static properties as early as possible static propTypes = { model: object.isRequired, title: string } // Default props below propTypes static defaultProps = { model: { id: 0 }, title: 'Your Name' } // Use fat arrow functions for methods to preserve context (this will thus be the component instance) handleSubmit = (e) => { e.preventDefault() this.props.model.save() } handleNameChange = (e) => { this.props.model.name = e.target.value } handleExpand = (e) => { e.preventDefault() this.setState(prevState => ({ expanded: !prevState.expanded })) } render() { // Destructure props for readability const { model, title } = this.props return ( <ExpandableForm onSubmit={this.handleSubmit} expanded={this.state.expanded} onExpand={this.handleExpand}> // Newline props if there are more than two <div> <h1>{title}</h1> <input type="text" value={model.name} // onChange={(e) => { model.name = e.target.value }} // Avoid creating new closures in the render method- use methods like below onChange={this.handleNameChange} placeholder="Your Name"/> </div> </ExpandableForm> ) } }
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.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
函数组件(Functional Components)
这些组件没有state和方法。它们是纯净的,非常容易定位问题,可以尽可能多的使用这些组件。
propTypes
复制
import React from 'react' import { observer } from 'mobx-react' import { func, bool } from 'prop-types' import './styles/Form.css' ExpandableForm.propTypes = { onSubmit: func.isRequired, expanded: bool } // Component declaration
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
这里我们在组件声明之前就定义了propTypes,非常直观。我们可以这么做是因为js的函数名提升机制。
Destructuring Props and defaultProps(解构props和defaultProps)
复制
import React from 'react' import { observer } from 'mobx-react' import { func, bool } from 'prop-types' import './styles/Form.css' ExpandableForm.propTypes = { onSubmit: func.isRequired, expanded: bool, onExpand: func.isRequired } function ExpandableForm(props) { const formStyle = props.expanded ? {height: 'auto'} : {height: 0} return ( <form style={formStyle} onSubmit={props.onSubmit}> {props.children} <button onClick={props.onExpand}>Expand</button> </form> ) }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
我们的组件是一个函数,props作为函数的入参被传递进来。我们可以按照如下方式对组件进行扩展:
复制
import React from 'react' import { observer } from 'mobx-react' import { func, bool } from 'prop-types' import './styles/Form.css' ExpandableForm.propTypes = { onSubmit: func.isRequired, expanded: bool, onExpand: func.isRequired } function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) { const formStyle = expanded ? {height: 'auto'} : {height: 0} return ( <form style={formStyle} onSubmit={onSubmit}> {children} <button onClick={onExpand}>Expand</button> </form> ) }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
我们可以给参数设置默认值,作为defaultProps。如果expanded是undefined,就将其设置为false。(这种设置默认值的方式,对于对象类的入参非常有用,可以避免`can't read property XXXX of undefined的错误)
不要使用es6箭头函数的写法:
复制
const ExpandableForm = ({ onExpand, expanded, children }) => {
1.
这种写法中,函数实际是匿名函数。如果正确地使用了babel则不成问题,但是如果没有,运行时就会导致一些错误,非常不方便调试。
另外,在Jest,一个react的测试库,中使用匿名函数也会导致一些问题。由于使用匿名函数可能会出现一些潜在的问题,我们推荐使用function,而不是const。
Wrapping
在函数组件中不能使用装饰器,我们可以将其作为入参传给observer函数
复制
import React from 'react' import { observer } from 'mobx-react' import { func, bool } from 'prop-types' import './styles/Form.css' ExpandableForm.propTypes = { onSubmit: func.isRequired, expanded: bool, onExpand: func.isRequired } function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) { const formStyle = expanded ? {height: 'auto'} : {height: 0} return ( <form style={formStyle} onSubmit={onSubmit}> {children} <button onClick={onExpand}>Expand</button> </form> ) } export default observer(ExpandableForm)
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
完整组件如下所示:
复制
import React from 'react' import { observer } from 'mobx-react' import { func, bool } from 'prop-types' // Separate local imports from dependencies import './styles/Form.css' // Declare propTypes here, before the component (taking advantage of JS function hoisting) // You want these to be as visible as possible ExpandableForm.propTypes = { onSubmit: func.isRequired, expanded: bool, onExpand: func.isRequired } // Destructure props like so, and use default arguments as a way of setting defaultProps function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) { const formStyle = expanded ? { height: 'auto' } : { height: 0 } return ( <form style={formStyle} onSubmit={onSubmit}> {children} <button onClick={onExpand}>Expand</button> </form> ) } // Wrap the component instead of decorating it export default observer(ExpandableForm)
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.
在JSX中使用条件判断(Conditionals in JSX)
有时候我们需要在render中写很多的判断逻辑,以下这种写法是我们应该要避免的:
目前有一些库来解决这个问题,但是我们没有引入其他依赖,而是采用了如下方式来解决:
这里我们采用立即执行函数的方式来解决问题,将if语句放到立即执行函数中,返回任何你想返回的。需要注意的是,立即执行函数会带来一定的性能问题,但是对于代码的可读性来说,这个影响可以忽略。
同样的,当你只希望在某种情况下渲染时,不要这么做:
复制
{ isTrue ? <p>True!</p> : <none/> }
1.
2.
3.
4.
5.
而应当这么做:
复制
{ isTrue && <p>True!</p> }
1.
2.
3.
4.
5.
6.
7.
(全文完)