项目作者: shimohq

项目描述 :
编写简洁漂亮,可维护的 React 应用
高级语言:
项目地址: git://github.com/shimohq/react-cookbook.git


React Cookbook

编写简洁漂亮,可维护的 React 应用

目录


前言

随着应用规模和维护人数的增加,光靠 React 本身灵活易用的 API 并不足以有效控制应用的复杂度。本指南旨在在 ESLint 之外,再建立一个我们团队内较为一致认可的约定,以增加代码一致性和可读性、降低维护成本。

欢迎在 Issues 进行相关讨论

组件声明

全面使用 ES6 class 声明,可不严格遵守该属性声明次序,但如有 propTypes 则必须写在顶部, lifecycle events 必须写到一起。

  • class
    • propTypes
    • defaultPropTypes
    • constructor
      • event handlers (如不使用类属性语法可在此声明)
    • lifecycle events
    • event handlers
    • getters
    • render
  1. class Person extends React.Component {
  2. static propTypes = {
  3. firstName: PropTypes.string.isRequired,
  4. lastName: PropTypes.string.isRequired
  5. }
  6. constructor (props) {
  7. super(props)
  8. this.state = { smiling: false }
  9. /* 若不能使用 babel-plugin-transform-class-properties
  10. this.handleClick = () => {
  11. this.setState({smiling: !this.state.smiling})
  12. }
  13. */
  14. }
  15. componentWillMount () {}
  16. componentDidMount () {}
  17. // ...
  18. handleClick = () => {
  19. this.setState({smiling: !this.state.smiling})
  20. }
  21. get fullName () {
  22. return this.props.firstName + this.props.lastName
  23. }
  24. render () {
  25. return (
  26. <div onClick={this.handleClick}>
  27. {this.fullName} {this.state.smiling ? 'is smiling.' : ''}
  28. </div>
  29. )
  30. }
  31. }

⬆ 回到目录

计算属性

使用 getters 封装 render 所需要的状态或条件的组合

对于返回 boolean 的 getter 使用 is- 前缀命名

  1. // bad
  2. render () {
  3. return (
  4. <div>
  5. {
  6. this.state.age > 18
  7. && (this.props.school === 'A'
  8. || this.props.school === 'B')
  9. ? <VipComponent ></VipComponent>
  10. : <NormalComponent ></NormalComponent>
  11. }
  12. </div>
  13. )
  14. }
  15. // good
  16. get isVIP() {
  17. return
  18. this.state.age > 18
  19. && (this.props.school === 'A'
  20. || this.props.school === 'B')
  21. }
  22. render() {
  23. return (
  24. <div>
  25. {this.isVIP ? <VipComponent ></VipComponent> : <NormalComponent ></NormalComponent>}
  26. </div>
  27. )
  28. }

⬆ 回到目录

事件回调命名

Handler 命名风格:

  • 使用 handle 开头
  • 以事件类型作为结尾 (如 Click, Change)
  • 使用一般现在时
  1. // bad
  2. closeAll = () => {},
  3. render () {
  4. return <div onClick={this.closeAll} ></div>
  5. }
  1. // good
  2. handleClick = () => {},
  3. render () {
  4. return <div onClick={this.handleClick} ></div>
  5. }

如果你需要区分同样事件类型的 handler(如 handleNameChangehandleEmailChange)时,可能这就是一个拆分组件的信号

⬆ 回到目录

组件化优于多层 render

当组件的 jsx 只写在一个 render 方法显得太臃肿时,很可能更适合拆分出一个组件,视情况采用 class component 或 stateless component

  1. // bad
  2. renderItem ({name}) {
  3. return (
  4. <li>
  5. {name}
  6. {/* ... */}
  7. </li>
  8. )
  9. }
  10. render () {
  11. return (
  12. <div className="menu">
  13. <ul>
  14. {this.props.items.map(item => this.renderItem(item))}
  15. </ul>
  16. </div>
  17. )
  18. }
  1. // good
  2. function Items ({name}) {
  3. return (
  4. <li>
  5. {name}
  6. {/* ... */}
  7. </li>
  8. )
  9. }
  10. render () {
  11. return (
  12. <div className="menu">
  13. <ul>
  14. {this.props.items.map(item => <Items {...item} ></Items>)}
  15. </ul>
  16. </div>
  17. )
  18. }

⬆ 回到目录

状态上移优于公共方法

一般组件不应提供公共方法,这样会破坏数据流只有一个方向的原则。

再因为我们倾向于更细颗粒的组件化,状态应集中在远离渲染的地方处理(比如应用级别的状态就在 redux 的 store 里),也能使兄弟组件更方便地共享。

  1. //bad
  2. class DropDownMenu extends Component {
  3. constructor (props) {
  4. super(props)
  5. this.state = {
  6. showMenu: false
  7. }
  8. }
  9. show () {
  10. this.setState({display: true})
  11. }
  12. hide () {
  13. this.setState({display: false})
  14. }
  15. render () {
  16. return this.state.display && (
  17. <div className="dropdown-menu">
  18. {/* ... */}
  19. </div>
  20. )
  21. }
  22. }
  23. class MyComponent extends Component {
  24. // ...
  25. showMenu () {
  26. this.refs.menu.show()
  27. }
  28. hideMenu () {
  29. this.refs.menu.hide()
  30. }
  31. render () {
  32. return <DropDownMenu ref="menu" ></DropDownMenu>
  33. }
  34. }
  35. //good
  36. class DropDownMenu extends Component {
  37. static propsType = {
  38. display: PropTypes.boolean.isRequired
  39. }
  40. render () {
  41. return this.props.display && (
  42. <div className="dropdown-menu">
  43. {/* ... */}
  44. </div>
  45. )
  46. }
  47. }
  48. class MyComponent extends Component {
  49. constructor (props) {
  50. super(props)
  51. this.state = {
  52. showMenu: false
  53. }
  54. }
  55. // ...
  56. showMenu () {
  57. this.setState({showMenu: true})
  58. }
  59. hideMenu () {
  60. this.setState({showMenu: false})
  61. }
  62. render () {
  63. return <DropDownMenu display={this.state.showMenu} ></DropDownMenu>
  64. }
  65. }

更多阅读: lifting-state-up

容器组件

一个容器组件主要负责维护状态和数据的计算,本身并没有界面逻辑,只把结果通过 props 传递下去。

区分容器组件的目的就是可以把组件的状态和渲染解耦开来,改写界面时可不用关注数据的实现,顺便得到了可复用性。

  1. // bad
  2. class MessageList extends Component {
  3. constructor (props) {
  4. super(props)
  5. this.state = {
  6. onlyUnread: false,
  7. messages: []
  8. }
  9. }
  10. componentDidMount () {
  11. $.ajax({
  12. url: "/api/messages",
  13. }).then(({messages}) => this.setState({messages}))
  14. }
  15. handleClick = () => this.setState({onlyUnread: !this.state.onlyUnread})
  16. render () {
  17. return (
  18. <div class="message">
  19. <ul>
  20. {
  21. this.state.messages
  22. .filter(msg => this.state.onlyUnread ? !msg.asRead : true)
  23. .map(({content, author}) => {
  24. return <li>{content}—{author}</li>
  25. })
  26. }
  27. </ul>
  28. <button onClick={this.handleClick}>toggle unread</button>
  29. </div>
  30. )
  31. }
  32. }
  1. // good
  2. class MessageContainer extends Component {
  3. constructor (props) {
  4. super(props)
  5. this.state = {
  6. onlyUnread: false,
  7. messages: []
  8. }
  9. }
  10. componentDidMount () {
  11. $.ajax({
  12. url: "/api/messages",
  13. }).then(({messages}) => this.setState({messages}))
  14. }
  15. handleClick = () => this.setState({onlyUnread: !this.state.onlyUnread})
  16. render () {
  17. return <MessageList
  18. messages={this.state.messages.filter(msg => this.state.onlyUnread ? !msg.asRead : true)}
  19. toggleUnread={this.handleClick}
  20. />
  21. }
  22. }
  23. function MessageList ({messages, toggleUnread}) {
  24. return (
  25. <div class="message">
  26. <ul>
  27. {
  28. messages
  29. .map(({content, author}) => {
  30. return <li>{content}—{author}</li>
  31. })
  32. }
  33. </ul>
  34. <button onClick={toggleUnread}>toggle unread</button>
  35. </div>
  36. )
  37. }
  38. MessageList.propTypes = {
  39. messages: propTypes.array.isRequired,
  40. toggleUnread: propTypes.func.isRequired
  41. }

更多阅读:

⬆ 回到目录

纯函数的 render

render 函数应该是一个纯函数(stateless component 当然也是),不依赖 this.state、this.props 以外的变量,也不改变外部状态

  1. // bad
  2. render () {
  3. return <div>{window.navigator.userAgent}</div>
  4. }
  5. // good
  6. render () {
  7. return <div>{this.props.userAgent}</div>
  8. }

更多阅读: @SimonRadionov/return-as-soon-as-you-know-the-answer-dec6369b9b67#.q67w8z60g">Return as soon as you know the answer

⬆ 回到目录

始终声明 PropTypes

每一个组件都声明 PropTypes,非必须的 props 应提供默认值。

对于非常广为人知的 props 如 children, dispatch 也不应该忽略。因为如果一个组件没有声明 dispatch 的 props,那么一眼就可以知道该组件没有修改 store 了。

但如果在开发一系列会 dispatch 的组件时,可在这些组件的目录建立单独的 .eslintrc 来只忽略 dispatch。

更多阅读: Prop Validation

⬆ 回到目录

Props 非空检测

对于并非 isRequired 的 proptype,必须对应设置 defaultProps,避免再增加 if 分支带来的负担

  1. // bad
  2. render () {
  3. if (this.props.person) {
  4. return <div>{this.props.person.firstName}</div>
  5. } else {
  6. return <div>Guest</div>
  7. }
  8. }
  1. // good
  2. class MyComponent extends Component {
  3. render() {
  4. return <div>{this.props.person.firstName}</div>
  5. }
  6. }
  7. MyComponent.defaultProps = {
  8. person: {
  9. firstName: 'Guest'
  10. }
  11. }

如有必要,使用 PropTypes.shape 明确指定需要的属性

⬆ 回到目录

使用 Props 初始化

除非 props 的命名明确指出了意图,否则不该使用 props 来初始化 state

  1. // bad
  2. constructor (props) {
  3. this.state = {
  4. items: props.items
  5. }
  6. }
  1. // good
  2. constructor (props) {
  3. this.state = {
  4. items: props.initialItems
  5. }
  6. }

更多阅读: “Props in getInitialState Is an Anti-Pattern”

⬆ 回到目录

classnames

使用 classNames 来组合条件结果.

  1. // bad
  2. render () {
  3. return <div className={'menu ' + this.props.display ? 'active' : ''} ></div>
  4. }
  1. // good
  2. render () {
  3. const classes = {
  4. menu: true,
  5. active: this.props.display
  6. }
  7. return <div className={classnames(classes)} ></div>
  8. }

Read: Class Name Manipulation

⬆ 回到目录