在我***次编写 React 代码的时候,我见发现许多不同的方法可以用来编写组件,不同教程教授的内容也大不相同。尽管从那时候起框架已经相当成熟,但并没有一种固定的“正确”方式指导。
在 MuseFind 工作的一年里,我们的团队编写了许多 React 组件,后期我们对方法进行了优化直到满意为止。
本指南描述了我们推荐的***实践,不管你是一名初学者还是有经验的老手,希望它能对你有所帮助。
在我们开始之前,有几个地方要注意一下:
我们使用的是 ES6 和 ES7 的语法。
如果你对于现实和容器组件两者之间的区别不甚明了,建议首先阅读一下这个。
如果有任何建议、疑问或者感想,请通过评论来让我们知晓。
基于类的组件具有丰富状态而且可以含有方法。我们要尽可能有节制地去使用,因为它们也有特定的适用场合。
让我们使用一行一行的代码逐步地将我们的组件构建起来吧。
复制
import React, {Component} from 'react'import {observer} from 'mobx-react'import ExpandableForm from './ExpandableForm'import './styles/ProfileContainer.css'
1.
2.
3.
4.
5.
我喜欢在 JavaScript 中操作 CSS,这在理论上这样做是可行的。不过它仍然是一种新的创意,还没出现切实可行的解决方案。不过在此之前,我们可以先为每一个组件引入一个 CSS 文件。
我们也通过另写一行将依赖引入从本地引入独立了出来。
复制
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.
复制
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 } static propTypes = { model: React.PropTypes.object.isRequired, title: React.PropTypes.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.
propTypes 和 defaultProps 是静态属性,要在组件代码中尽可能高的位置进行声明。它们作为文档放在醒目的位置,其他开发者阅读此文件时能立即看到。
所有的组件都应该有 propTypes。
复制
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 } static propTypes = { model: React.PropTypes.object.isRequired, title: React.PropTypes.string } static defaultProps = { model: { id: 0 }, title: 'Your Name' } handleSubmit = (e) => { e.preventDefault() this.props.model.save() } handleNameChange = (e) => { this.props.model.name = 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.
有了类组件,在你想子组件传递方法时,就得去确认它们在被调用到时所持有的 this 对象是正确的。这个一般可以通过将 this.handleSubmit.bind(this) 传递给子组件来达成。
我们认为这种方式更加干净且容易,借助 ES6 的箭头函数可以自动地维护好正确的上线文。
复制
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 } static propTypes = { model: React.PropTypes.object.isRequired, title: React.PropTypes.string } static defaultProps = { model: { id: 0 }, title: 'Your Name' } 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() { 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.
拥有许多属性的组件要让每个属性都另起一行,如上所示。
复制
@observerexport default class ProfileContainer extends Component {
1.
2.
如果你使用了一些像 mobx 的东西,就可以像上面这样对类组件进行装饰 — 这样做跟将组件传递给一个函数是一样的效果。
装饰器是一种用来修改组件功能的灵活且可读性好的方式。我们对其进行了广泛的运用,包括 mobx 还有我们自己的 mobx-models 库。
如果你不想使用装饰器,可以这样做:
复制
class ProfileContainer extends Component { // Component code}export default observer(ProfileContainer)
1.
2.
3.
4.
要避免向子组件传递新的闭包,如下:
复制
<input type="text" value={model.name} // onChange={(e) => { model.name = e.target.value }} // ^ Not this. Use the below: onChange={this.handleChange} placeholder="Your Name"/>
1.
2.
3.
4.
5.
6.
7.
原因: 每次父组件渲染时,都会有一个新的函数被创建并传递给输入。
如果输入是一个 React 组件,不管它的其它属性实际是否已经发生了变化,都会自动地触发让它重新渲染。
调和是 React 中消耗最昂贵的部分,因此不要让它的计算难度超过所需! 另外,传递一个类方法更容易阅读、调试和修改。
如下是完整的组件代码:
复制
import React, {Component} from 'react'import {observer} from 'mobx-react'// Separate local imports from dependenciesimport ExpandableForm from './ExpandableForm'import './styles/ProfileContainer.css'// Use decorators if needed@observerexport 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: React.PropTypes.object.isRequired, title: React.PropTypes.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.
这些组件没有状态和方法。它们就是单纯的组件,容易理解。要尽可能常去使用它们。
复制
import React from 'react'import {observer} from 'mobx-react'import './styles/Form.css'const expandableFormRequiredProps = { onSubmit: React.PropTypes.func.isRequired, expanded: React.PropTypes.bool }// Component declarationExpandableForm.propTypes = expandableFormRequiredProps
1.
2.
3.
4.
5.
6.
7.
8.
9.
这里,我们将 propTypes 分配给了顶部一行的变量。在组件声明的下面,我们对它们进行了正常的分配。
复制
import React from 'react'import {observer} from 'mobx-react'import './styles/Form.css'const expandableFormRequiredProps = { onSubmit: React.PropTypes.func.isRequired, expanded: React.PropTypes.bool }function ExpandableForm(props) { return ( <form style={props.expanded ? {height: 'auto'} : {height: 0}}> {props.children} <button onClick={props.onExpand}>Expand</button> </form> ) }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
我的组件是一个函数,因此可以将它的属性看做是参数。我们可以像下面这样对它们进行扩展:
复制
import React from 'react'import {observer} from 'mobx-react'import './styles/Form.css'const expandableFormRequiredProps = { onExpand: React.PropTypes.func.isRequired, expanded: React.PropTypes.bool }function ExpandableForm({ onExpand, expanded = false, children }) { return ( <form style={ expanded ? { height: 'auto' } : { height: 0 } }> {children} <button onClick={onExpand}>Expand</button> </form> ) }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
注意,我们也能以一种高度可读的方式使用默认参数来扮演 defaultProps 的角色。如果 expanded 是 undefined, 我们就会将其设置为 false。 (这个例子有点勉强,因为是一个布尔值,不过本身对于避免对象的“Cannot read <property> of undefined“这样的错误是很有用的)。
要避免如下这种 ES6 语法:
复制
const ExpandableForm = ({ onExpand, expanded, children }) => {
1.
看着非常现代,不过这里的函数实际上没有被命令。
这样子的名称在 Bable 进行了正确的设置的情况下是可行的 — 但如果没有正确设置,任何错误都会以在<<anonymous>>中出现的方式显示,调试起来相当麻烦。
无名的函数也会在 Jest 这个 React 测试库中引发问题。为了避免潜在的复杂问题出现,我们建议使用 function 而不是 const。
因为在函数式组件中不能使用装饰器,所以你可以简单地将它传递到函数中充当参数:
复制
import React from 'react'import {observer} from 'mobx-react'import './styles/Form.css'const expandableFormRequiredProps = { onExpand: React.PropTypes.func.isRequired, expanded: React.PropTypes.bool }function ExpandableForm({ onExpand, expanded = false, children }) { return ( <form style={ expanded ? { height: 'auto' } : { height: 0 } }> {children} <button onClick={onExpand}>Expand</button> </form> ) } ExpandableForm.propTypes = expandableFormRequiredPropsexport default observer(ExpandableForm)
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
如下是完整的组件代码:
复制
import React from 'react'import {observer} from 'mobx-react'// Separate local imports from dependenciesimport './styles/Form.css'// Declare propTypes here as a variable, then assign below function declaration // You want these to be as visible as possibleconst expandableFormRequiredProps = { onSubmit: React.PropTypes.func.isRequired, expanded: React.PropTypes.bool }// Destructure props like so, and use default arguments as a way of setting defaultPropsfunction ExpandableForm({ onExpand, expanded = false, children }) { return ( <form style={ expanded ? { height: 'auto' } : { height: 0 } }> {children} <button onClick={onExpand}>Expand</button> </form> ) }// Set propTypes down here to those declared aboveExpandableForm.propTypes = expandableFormRequiredProps// Wrap the component instead of decorating itexport 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.
你会有不少机会去做许多条件分支渲染。如下是你想要去避免的情况:
嵌套的三元组并非不是好主意。
有一些库可以解决这个问题 (JSX-Control Statements),不过相比引入额外的依赖,通过如下这种方式解决复杂条件分支问题要更好:
使用花括弧封装一个 IIFE, 然后在里面放入 if 语句,可以返回任何你想要渲染的东西。注意像这样的 IIFE 对性能会有影响,不过在大多数情况中还不足以让我们为此选择丢掉可读性。
还有就是当你只想要在一个条件分支中渲染一个元素时,比起这样做…
复制
{ isTrue ? <p>True!</p> : <none/>}
1.
2.
3.
4.
5.
… 使用短路写法更划算:
复制
{ isTrue && <p>True!</p>}
1.
2.
3.
4.