编写简洁漂亮,可维护的 React 应用
- 前言
- 组件声明
- 计算属性
- 事件回调命名
- 组件化优于多层 render
- 状态上移优于公共方法
- 容器组件
- 纯函数的 render
- 始终声明 PropTypes
- Props 非空检测
- 使用 Props 初始化
- classnames
随着应用规模和维护人数的增加,光靠 React 本身灵活易用的 API 并不足以有效控制应用的复杂度。本指南旨在在 ESLint 之外,再建立一个我们团队内较为一致认可的约定,以增加代码一致性和可读性、降低维护成本。
欢迎在 Issues 进行相关讨论
全面使用 ES6 class 声明,可不严格遵守该属性声明次序,但如有 propTypes 则必须写在顶部, lifecycle events 必须写到一起。
- class
- propTypes
- defaultPropTypes
- constructor
- event handlers (如不使用类属性语法可在此声明)
- lifecycle events
- event handlers
- getters
- render
classPersonextendsReact.Component{staticpropTypes={firstName: PropTypes.string.isRequired,lastName: PropTypes.string.isRequired}constructor(props){super(props)this.state={smiling: false}/* 若不能使用 babel-plugin-transform-class-properties this.handleClick = () => { this.setState({smiling: !this.state.smiling}) } */}componentWillMount(){}componentDidMount(){}// ...handleClick=()=>{this.setState({smiling: !this.state.smiling})}getfullName(){returnthis.props.firstName+this.props.lastName}render(){return(<divonClick={this.handleClick}>{this.fullName}{this.state.smiling ? 'is smiling.' : ''}</div>)}}使用 getters 封装 render 所需要的状态或条件的组合
对于返回 boolean 的 getter 使用 is- 前缀命名
// badrender(){return(<div>{this.state.age>18&&(this.props.school==='A'||this.props.school==='B')
? <VipComponent/>
: <NormalComponent/>}</div>)}// goodgetisVIP(){returnthis.state.age>18&&(this.props.school==='A'||this.props.school==='B')}render(){return(<div>{this.isVIP ? <VipComponent/> : <NormalComponent/>}</div>)}Handler 命名风格:
- 使用
handle开头 - 以事件类型作为结尾 (如
Click,Change) - 使用一般现在时
// badcloseAll=()=>{},render(){return<divonClick={this.closeAll}/>}// goodhandleClick=()=>{},render(){return<divonClick={this.handleClick}/>}如果你需要区分同样事件类型的 handler(如 handleNameChange 和 handleEmailChange)时,可能这就是一个拆分组件的信号
当组件的 jsx 只写在一个 render 方法显得太臃肿时,很可能更适合拆分出一个组件,视情况采用 class component 或 stateless component
// badrenderItem({name}){return(<li>{name}{/* ... */}</li>)}render(){return(<divclassName="menu"><ul>{this.props.items.map(item=>this.renderItem(item))}</ul></div>)}// goodfunctionItems({name}){return(<li>{name}{/* ... */}</li>)}render(){return(<divclassName="menu"><ul>{this.props.items.map(item=><Items{...item}/>)}</ul></div>)}一般组件不应提供公共方法,这样会破坏数据流只有一个方向的原则。
再因为我们倾向于更细颗粒的组件化,状态应集中在远离渲染的地方处理(比如应用级别的状态就在 redux 的 store 里),也能使兄弟组件更方便地共享。
//badclassDropDownMenuextendsComponent{constructor(props){super(props)this.state={showMenu: false}}show(){this.setState({display: true})}hide(){this.setState({display: false})}render(){returnthis.state.display&&(<divclassName="dropdown-menu">{/* ... */}</div>)}}classMyComponentextendsComponent{// ...showMenu(){this.refs.menu.show()}hideMenu(){this.refs.menu.hide()}render(){return<DropDownMenuref="menu"/>}}//goodclassDropDownMenuextendsComponent{staticpropsType={display: PropTypes.boolean.isRequired}render(){returnthis.props.display&&(<divclassName="dropdown-menu">{/* ... */}</div>)}}classMyComponentextendsComponent{constructor(props){super(props)this.state={showMenu: false}}// ...showMenu(){this.setState({showMenu: true})}hideMenu(){this.setState({showMenu: false})}render(){return<DropDownMenudisplay={this.state.showMenu}/>}}更多阅读: lifting-state-up
一个容器组件主要负责维护状态和数据的计算,本身并没有界面逻辑,只把结果通过 props 传递下去。
区分容器组件的目的就是可以把组件的状态和渲染解耦开来,改写界面时可不用关注数据的实现,顺便得到了可复用性。
// badclassMessageListextendsComponent{constructor(props){super(props)this.state={onlyUnread: false,messages: []}}componentDidMount(){$.ajax({url: "/api/messages",}).then(({messages})=>this.setState({messages}))}handleClick=()=>this.setState({onlyUnread: !this.state.onlyUnread})render(){return(<divclass="message"><ul>{this.state.messages.filter(msg=>this.state.onlyUnread ? !msg.asRead : true).map(({content, author})=>{return<li>{content}—{author}</li>})}</ul><buttononClick={this.handleClick}>toggle unread</button></div>)}}// goodclassMessageContainerextendsComponent{constructor(props){super(props)this.state={onlyUnread: false,messages: []}}componentDidMount(){$.ajax({url: "/api/messages",}).then(({messages})=>this.setState({messages}))}handleClick=()=>this.setState({onlyUnread: !this.state.onlyUnread})render(){return<MessageListmessages={this.state.messages.filter(msg=>this.state.onlyUnread ? !msg.asRead : true)}toggleUnread={this.handleClick}/>}}functionMessageList({messages, toggleUnread}){return(<divclass="message"><ul>{messages.map(({content, author})=>{return<li>{content}—{author}</li>})}</ul><buttononClick={toggleUnread}>toggle unread</button></div>)}MessageList.propTypes={messages: propTypes.array.isRequired,toggleUnread: propTypes.func.isRequired}更多阅读:
render 函数应该是一个纯函数(stateless component 当然也是),不依赖 this.state、this.props 以外的变量,也不改变外部状态
// badrender(){return<div>{window.navigator.userAgent}</div>}// goodrender(){return<div>{this.props.userAgent}</div>}更多阅读: Return as soon as you know the answer
每一个组件都声明 PropTypes,非必须的 props 应提供默认值。
对于非常广为人知的 props 如 children, dispatch 也不应该忽略。因为如果一个组件没有声明 dispatch 的 props,那么一眼就可以知道该组件没有修改 store 了。
但如果在开发一系列会 dispatch 的组件时,可在这些组件的目录建立单独的 .eslintrc 来只忽略 dispatch。
更多阅读: Prop Validation
对于并非 isRequired 的 proptype,必须对应设置 defaultProps,避免再增加 if 分支带来的负担
// badrender(){if(this.props.person){return<div>{this.props.person.firstName}</div>}else{return<div>Guest</div>}}// goodclassMyComponentextendsComponent{render(){return<div>{this.props.person.firstName}</div>}}MyComponent.defaultProps={person: {firstName: 'Guest'}}如有必要,使用 PropTypes.shape 明确指定需要的属性
除非 props 的命名明确指出了意图,否则不该使用 props 来初始化 state
// badconstructor(props){this.state={items: props.items}}// goodconstructor(props){this.state={items: props.initialItems}}更多阅读: "Props in getInitialState Is an Anti-Pattern"
使用 classNames 来组合条件结果.
// badrender(){return<divclassName={'menu '+this.props.display ? 'active' : ''}/>}// goodrender(){letclasses={menu: true,active: this.props.display}return<divclassName={classnames(classes)}/>}Read: Class Name Manipulation