{"id":13679305,"url":"https://github.com/shimohq/react-cookbook","last_synced_at":"2025-04-05T02:09:20.037Z","repository":{"id":11524810,"uuid":"69949804","full_name":"shimohq/react-cookbook","owner":"shimohq","description":"编写简洁漂亮，可维护的 React 应用","archived":false,"fork":false,"pushed_at":"2021-10-05T07:29:07.000Z","size":8,"stargazers_count":625,"open_issues_count":3,"forks_count":73,"subscribers_count":25,"default_branch":"master","last_synced_at":"2025-03-29T01:09:59.230Z","etag":null,"topics":["clean-code","component","cookbook","react","styleguide"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/shimohq.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-10-04T09:12:38.000Z","updated_at":"2024-11-18T23:46:55.000Z","dependencies_parsed_at":"2022-07-21T16:02:33.031Z","dependency_job_id":null,"html_url":"https://github.com/shimohq/react-cookbook","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shimohq%2Freact-cookbook","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shimohq%2Freact-cookbook/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shimohq%2Freact-cookbook/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shimohq%2Freact-cookbook/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/shimohq","download_url":"https://codeload.github.com/shimohq/react-cookbook/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247276164,"owners_count":20912288,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clean-code","component","cookbook","react","styleguide"],"created_at":"2024-08-02T13:01:04.091Z","updated_at":"2025-04-05T02:09:20.015Z","avatar_url":"https://github.com/shimohq.png","language":null,"funding_links":[],"categories":["Javascript","Others","Others (1002)"],"sub_categories":[],"readme":"\nReact Cookbook\n=====\n\n*编写简洁漂亮，可维护的 React 应用*\n\n## 目录\n- [前言](#前言)\n- [组件声明](#组件声明)\n- [计算属性](#计算属性)\n- [事件回调命名](#事件回调命名)\n- [组件化优于多层 render](#组件化优于多层-render)\n- [状态上移优于公共方法](#状态上移优于公共方法)\n- [容器组件](#容器组件)\n- [纯函数的 render](#纯函数的-render)\n- [始终声明 PropTypes](#始终声明-proptypes)\n- [Props 非空检测](#props-非空检测)\n- [使用 Props 初始化](#使用-props-初始化)\n- [classnames](#classnames)\n\n---\n\n## 前言\n\n随着应用规模和维护人数的增加，光靠 React 本身灵活易用的 API 并不足以有效控制应用的复杂度。本指南旨在在 ESLint 之外，再建立一个我们团队内较为一致认可的约定，以增加代码一致性和可读性、降低维护成本。\n\n_欢迎在 [Issues](https://github.com/shimohq/react-cookbook/issues) 进行相关讨论_\n\n## 组件声明\n\n全面使用 ES6 class 声明，可不严格遵守该属性声明次序，但如有 propTypes 则必须写在顶部， lifecycle events 必须写到一起。\n\n* class\n  * propTypes\n  * defaultPropTypes\n  * constructor\n    * event handlers (如不使用[类属性](http://babeljs.io/docs/plugins/transform-class-properties/)语法可在此声明)\n  * lifecycle events\n  * event handlers\n  * getters\n  * render\n\n```javascript\nclass Person extends React.Component {\n  static propTypes = {\n    firstName: PropTypes.string.isRequired,\n    lastName: PropTypes.string.isRequired\n  }\n  constructor (props) {\n    super(props)\n\n    this.state = { smiling: false }\n\n    /* 若不能使用 babel-plugin-transform-class-properties\n    this.handleClick = () =\u003e {\n      this.setState({smiling: !this.state.smiling})\n    }\n    */\n  }\n\n  componentWillMount () {}\n\n  componentDidMount () {}\n\n  // ...\n\n  handleClick = () =\u003e {\n    this.setState({smiling: !this.state.smiling})\n  }\n\n  get fullName () {\n    return this.props.firstName + this.props.lastName\n  }\n\n  render () {\n    return (\n      \u003cdiv onClick={this.handleClick}\u003e\n        {this.fullName} {this.state.smiling ? 'is smiling.' : ''}\n      \u003c/div\u003e\n    )\n  }\n}\n```\n\n**[⬆ 回到目录](#目录)**\n\n## 计算属性\n\n使用 getters 封装 render 所需要的状态或条件的组合\n\n对于返回 boolean 的 getter 使用 is- 前缀命名\n\n```javascript\n  // bad\n  render () {\n    return (\n      \u003cdiv\u003e\n        {\n          this.state.age \u003e 18\n            \u0026\u0026 (this.props.school === 'A'\n              || this.props.school === 'B')\n            ? \u003cVipComponent /\u003e\n            : \u003cNormalComponent /\u003e\n        }\n      \u003c/div\u003e\n    )\n  }\n\n  // good\n  get isVIP() {\n    return\n      this.state.age \u003e 18\n        \u0026\u0026 (this.props.school === 'A'\n          || this.props.school === 'B')\n  }\n  render() {\n    return (\n      \u003cdiv\u003e\n        {this.isVIP ? \u003cVipComponent /\u003e : \u003cNormalComponent /\u003e}\n      \u003c/div\u003e\n    )\n  }\n```\n\n**[⬆ 回到目录](#目录)**\n\n## 事件回调命名\n\nHandler 命名风格:\n\n- 使用 `handle` 开头\n- 以事件类型作为结尾 (如 `Click`, `Change`)\n- 使用一般现在时\n\n```javascript\n// bad\ncloseAll = () =\u003e {},\n\nrender () {\n  return \u003cdiv onClick={this.closeAll} /\u003e\n}\n```\n\n```javascript\n// good\nhandleClick = () =\u003e {},\n\nrender () {\n  return \u003cdiv onClick={this.handleClick} /\u003e\n}\n```\n\n如果你需要区分同样事件类型的 handler（如 `handleNameChange` 和 `handleEmailChange`）时，可能这就是一个拆分组件的信号\n\n**[⬆ 回到目录](#目录)**\n\n## 组件化优于多层 render\n\n当组件的 jsx 只写在一个 render 方法显得太臃肿时，很可能更适合拆分出一个组件，视情况采用 class component 或 stateless component\n\n```javascript\n// bad\nrenderItem ({name}) {\n  return (\n    \u003cli\u003e\n    \t{name}\n    \t{/* ... */}\n    \u003c/li\u003e\n  )\n}\n\nrender () {\n  return (\n    \u003cdiv className=\"menu\"\u003e\n      \u003cul\u003e\n        {this.props.items.map(item =\u003e this.renderItem(item))}\n      \u003c/ul\u003e\n    \u003c/div\u003e\n  )\n}\n```\n\n```javascript\n// good\nfunction Items ({name}) {\n  return (\n    \u003cli\u003e\n    \t{name}\n    \t{/* ... */}\n    \u003c/li\u003e\n  )\n}\n\nrender () {\n  return (\n    \u003cdiv className=\"menu\"\u003e\n      \u003cul\u003e\n        {this.props.items.map(item =\u003e \u003cItems {...item} /\u003e)}\n      \u003c/ul\u003e\n    \u003c/div\u003e\n  )\n}\n```\n\n**[⬆ 回到目录](#目录)**\n\n## 状态上移优于公共方法\n\n一般组件不应提供公共方法，这样会破坏数据流只有一个方向的原则。\n\n再因为我们倾向于更细颗粒的组件化，状态应集中在远离渲染的地方处理（比如应用级别的状态就在 redux 的 store 里），也能使兄弟组件更方便地共享。\n\n```javascript\n//bad\nclass DropDownMenu extends Component {\n  constructor (props) {\n    super(props)\n    this.state = {\n      showMenu: false\n    }\n  }\n\n  show () {\n    this.setState({display: true})\n  }\n\n  hide () {\n    this.setState({display: false})\n  }\n\n  render () {\n    return this.state.display \u0026\u0026 (\n      \u003cdiv className=\"dropdown-menu\"\u003e\n        {/* ... */}\n      \u003c/div\u003e\n    )\n  }\n}\n\nclass MyComponent extends Component {\n  // ...\n  showMenu () {\n    this.refs.menu.show()\n  }\n  hideMenu () {\n    this.refs.menu.hide()\n  }\n  render () {\n    return \u003cDropDownMenu ref=\"menu\" /\u003e\n  }\n}\n\n//good\nclass DropDownMenu extends Component {\n  static propsType = {\n    display: PropTypes.boolean.isRequired\n  }\n\n  render () {\n    return this.props.display \u0026\u0026 (\n      \u003cdiv className=\"dropdown-menu\"\u003e\n        {/* ... */}\n      \u003c/div\u003e\n    )\n  }\n}\n\nclass MyComponent extends Component {\n  constructor (props) {\n    super(props)\n    this.state = {\n      showMenu: false\n    }\n  }\n\n  // ...\n\n  showMenu () {\n    this.setState({showMenu: true})\n  }\n\n  hideMenu () {\n    this.setState({showMenu: false})\n  }\n\n  render () {\n    return \u003cDropDownMenu display={this.state.showMenu} /\u003e\n  }\n}\n```\n\n更多阅读: [lifting-state-up](https://facebook.github.io/react/docs/lifting-state-up.html)\n\n## 容器组件\n\n一个容器组件主要负责维护状态和数据的计算，本身并没有界面逻辑，只把结果通过 props 传递下去。\n\n区分容器组件的目的就是可以把组件的状态和渲染解耦开来，改写界面时可不用关注数据的实现，顺便得到了可复用性。\n\n```javascript\n// bad\nclass MessageList extends Component {\n  constructor (props) {\n    super(props)\n  \tthis.state = {\n        onlyUnread: false,\n        messages: []\n  \t}\n  }\n\n  componentDidMount () {\n    $.ajax({\n      url: \"/api/messages\",\n    }).then(({messages}) =\u003e this.setState({messages}))\n  }\n\n  handleClick = () =\u003e this.setState({onlyUnread: !this.state.onlyUnread})\n\n  render () {\n    return (\n      \u003cdiv class=\"message\"\u003e\n        \u003cul\u003e\n          {\n            this.state.messages\n              .filter(msg =\u003e this.state.onlyUnread ? !msg.asRead : true)\n              .map(({content, author}) =\u003e {\n                return \u003cli\u003e{content}—{author}\u003c/li\u003e\n              })\n          }\n        \u003c/ul\u003e\n        \u003cbutton onClick={this.handleClick}\u003etoggle unread\u003c/button\u003e\n      \u003c/div\u003e\n    )\n  }\n}\n```\n\n```javascript\n// good\nclass MessageContainer extends Component {\n  constructor (props) {\n    super(props)\n  \tthis.state = {\n        onlyUnread: false,\n        messages: []\n  \t}\n  }\n\n  componentDidMount () {\n    $.ajax({\n      url: \"/api/messages\",\n    }).then(({messages}) =\u003e this.setState({messages}))\n  }\n\n  handleClick = () =\u003e this.setState({onlyUnread: !this.state.onlyUnread})\n\n  render () {\n    return \u003cMessageList\n      messages={this.state.messages.filter(msg =\u003e this.state.onlyUnread ? !msg.asRead : true)}\n      toggleUnread={this.handleClick}\n    /\u003e\n  }\n}\n\nfunction MessageList ({messages, toggleUnread}) {\n  return (\n    \u003cdiv class=\"message\"\u003e\n      \u003cul\u003e\n        {\n          messages\n            .map(({content, author}) =\u003e {\n              return \u003cli\u003e{content}—{author}\u003c/li\u003e\n            })\n        }\n      \u003c/ul\u003e\n      \u003cbutton onClick={toggleUnread}\u003etoggle unread\u003c/button\u003e\n    \u003c/div\u003e\n  )\n}\nMessageList.propTypes = {\n  messages: propTypes.array.isRequired,\n  toggleUnread: propTypes.func.isRequired\n}\n```\n\n更多阅读:\n- [Presentational and Container Components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.sz7z538t6)\n- [React AJAX Best Practices](http://andrewhfarmer.com/react-ajax-best-practices/)\n\n**[⬆ 回到目录](#目录)**\n\n## 纯函数的 render\n\nrender 函数应该是一个纯函数（stateless component 当然也是），不依赖 this.state、this.props 以外的变量，也不改变外部状态\n\n```javascript\n// bad\nrender () {\n  return \u003cdiv\u003e{window.navigator.userAgent}\u003c/div\u003e\n}\n\n// good\nrender () {\n  return \u003cdiv\u003e{this.props.userAgent}\u003c/div\u003e\n}\n```\n\n更多阅读: [Return as soon as you know the answer](https://medium.com/@SimonRadionov/return-as-soon-as-you-know-the-answer-dec6369b9b67#.q67w8z60g)\n\n**[⬆ 回到目录](#目录)**\n\n## 始终声明 PropTypes\n\n每一个组件都声明 PropTypes，非必须的 props 应提供默认值。\n\n对于非常广为人知的 props 如 children, dispatch 也不应该忽略。因为如果一个组件没有声明 dispatch 的 props，那么一眼就可以知道该组件没有修改 store 了。\n\n但如果在开发一系列会 dispatch 的组件时，可在这些组件的目录建立单独的 .eslintrc 来只忽略 dispatch。\n\n更多阅读: [Prop Validation](http://facebook.github.io/react/docs/reusable-components.html#prop-validation)\n\n**[⬆ 回到目录](#目录)**\n\n## Props 非空检测\n\n对于并非 `isRequired` 的 proptype，必须对应设置 defaultProps，避免再增加 if 分支带来的负担\n\n```javascript\n// bad\nrender () {\n  if (this.props.person) {\n    return \u003cdiv\u003e{this.props.person.firstName}\u003c/div\u003e\n  } else {\n    return \u003cdiv\u003eGuest\u003c/div\u003e\n  }\n}\n```\n\n```javascript\n// good\nclass MyComponent extends Component {\n  render() {\n    return \u003cdiv\u003e{this.props.person.firstName}\u003c/div\u003e\n  }\n}\n\nMyComponent.defaultProps = {\n  person: {\n    firstName: 'Guest'\n  }\n}\n```\n\n如有必要，使用 PropTypes.shape 明确指定需要的属性\n\n**[⬆ 回到目录](#目录)**\n\n## 使用 Props 初始化\n\n除非 props 的命名明确指出了意图，否则不该使用 props 来初始化 state\n\n```javascript\n// bad\nconstructor (props) {\n  this.state = {\n    items: props.items\n  }\n}\n```\n\n```javascript\n// good\nconstructor (props) {\n  this.state = {\n    items: props.initialItems\n  }\n}\n```\n\n更多阅读: [\"Props in getInitialState Is an Anti-Pattern\"](http://facebook.github.io/react/tips/props-in-getInitialState-as-anti-pattern.html)\n\n**[⬆ 回到目录](#目录)**\n\n## classnames\n\n使用 [classNames](https://www.npmjs.com/package/classnames) 来组合条件结果.\n\n```javascript\n// bad\nrender () {\n  return \u003cdiv className={'menu ' + this.props.display ? 'active' : ''} /\u003e\n}\n```\n\n```javascript\n// good\nrender () {\n  const classes = {\n    menu: true,\n    active: this.props.display\n  }\n\n  return \u003cdiv className={classnames(classes)} /\u003e\n}\n```\n\nRead: [Class Name Manipulation](https://github.com/JedWatson/classnames/blob/master/README.md)\n\n**[⬆ 回到目录](#目录)**\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshimohq%2Freact-cookbook","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fshimohq%2Freact-cookbook","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshimohq%2Freact-cookbook/lists"}