0
点赞
收藏
分享

微信扫一扫

React最佳实践

1 多用 Function Component

如果组件是纯展示型的,不需要维护 state 和生命周期,则优先使用 Function Component。它有如下好处:

  1. 代码更简洁,一看就知道是纯展示型的,没有复杂的业务逻辑
  2. 更好的复用性。只要传入相同结构的 props,就能展示相同的界面,不需要考虑副作用。
  3. 更小的打包体积,更高的执行效率

一个典型的 Function Component 是下面这个样子:

function MenuItem({menuId, menuText, onClick, activeId}) {
    return (
        <div
            menuId={menuId}
            className={`${style} ${activeId === menuId ? active : ''}`}
            onClick={onItemClick}
        >
            {menuText}
        </div>
    );
};

2 多用 PureComponent

如果组件需要维护 state 或使用生命周期方法,则优先使用 PureComponent,而不是 Component。Component 的默认行为是不论 state 和 props 是否有变化,都触发 render。而 PureComponent 会先对 state 和 props 进行浅比较,不同的时候才会 render。请看下面的例子:

class Child extends React.Component {
  render() {
    console.log('render Child');
    return (
      <div>
        {this.props.obj.num}
      </div>
    );
  }
}

class App extends React.Component {
  state = {
    obj: { num: 1 }
  };

  onClick = () => {
    const {obj} = this.state;
    this.setState({obj});
  }

  render() {
    console.log('render Parent');
    return (
      <div className="App" >
        <button onClick={this.onClick}>
          点我
        </button>
        <Child obj={this.state.obj}/>
      </div>
    );
  }
}

点击按钮后,Parent 和 Child 的 render 都会触发。如果将 Child 改为 PureComponent,则 Child 的 render 不会触发,因为 props 还是同一个对象。如果将 Parent 也改为 PureComponent,则 Parent 的 render 也不会触发了,因为 state 还是同一个对象。

3 遵循单一职责原则,使用 HOC / 装饰器 / Render Props 增加职责

比如一个公用的组件,数据来源可能是父组件传过来,又或者是自己主动通过网络请求获取数据。这时候可以先定义一个纯展示型的 Function Component,然后再定义一个高阶组件去获取数据:

function Comp() {
    ...
}

class HOC extends PureComponent {

    async componentDidMount() {
        const data = await fetchData();
        this.setState({data});
    }
    
    render() {
        return (<Comp data={this.state.data}/>);
    }
}

4 组合优于继承

以继承的形式写组件,自己写得很爽,代码的复用性也很好,但最大的问题是别人看不懂。我将复用的业务逻辑和 UI 模版都在父类定义好,子类只需要传入一些参数,然后再覆盖父类的几个方法就好(render的时候会用到)。简化的代码如下:

class Parent extends PureComponent {
    componentDidMount() {
        this.fetchData(this.url);
    }
    
    fetchData(url) {
        ...
    }
    
    render() {
        const data = this.calcData();
        return (
            <div>{data}</data>
        );
    }
}

class Child extends Parent {
    constructor(props) {
        super(props);
        this.url = 'http://api';
    }
    
    calcData() {
        ...
    }
}

这样的写法从语言的特性和功能实现来说,没有任何问题,最大的问题是不符合 React 的组件编写习惯。父类或者子类肯定有一方是不需要实现 render 方法的,而一般我们看代码都会优先找 render 方法,找不到就慌了。另外就是搞不清楚哪些方法是父类实现的,哪些方法是子类实现的,如果让其他人来维护这份代码,会比较吃力。

继承会让代码难以溯源,定位问题也比较麻烦。所有通过继承实现的组件都可以改写为组合的形式。上面的代码就可以这样改写:

class Parent extends PureComponent {
    componentDidMount() {
        this.fetchData(this.props.url);
    }
    
    fetchData(url) {
        ...
    }
    
    render() {
        const data = this.props.calcData(this.state);
        return (
            <div>{data}</data>
        );
    }
}

class Child extends PureComponent {
    calcData(state) {
        ...
    }
    
    render() {
        <Parent url="http://api" calcData={this.calcData}/>
    }
}

5 如果 props 的数据不会改变,就不需要在 state 或者组件实例属性里拷贝一份

经常会看见这样的代码:

componentWillReceiveProps(nextProps) {
    this.setState({num: nextProps.num});
}

render() {
    return(
        <div>{this.state.num}</div>
    );
}

num 在组件中不会做任何的改变,这种情况下直接使用 this.props.num 就可以了。

6 避免在 render 里面动态创建对象 / 方法,否则会导致子组件每次都 render

render() {
    const obj = {num: 1}
    
    return(
        <Child obj={obj} onClick={()=>{...}} />
    );
}

在上面代码中,即使 Child 是 PureComponent,由于 obj 和 onClick 每次 render 都是新的对象,Child 也会跟着 render。

7 避免在 JSX 中写复杂的三元表达式,应通过封装函数或组件实现

render() {
    const a = 8;
    
    return (
        <div>
            {
                a > 0 ? a < 9 ? ... : ... : ...
            }
        </div>
    );    
}

像上面这种嵌套的三元表达式可读性非常差,可以写成下面的形式:

f() {
    ...
}

render() {
    const a = 8;
    
    return (
        <div>
            {
                this.f()
            }
        </div>
    );    
}

8 定义组件时,定义 PropTypes 和 defaultProps

class CategorySelector extends PureComponent {
    ...
}

CategorySelector.propTypes = {
    type: PropTypes.string,
    catList: PropTypes.array.isRequired,
    default: PropTypes.bool,
};

CategorySelector.defaultProps = {
    default: false,
    type: undefined,
};

9 避免使用无谓的标签和样式

下面这种情况一般外层的div是多余的,可以将样式直接定义在组件内,或者将定制的样式作为参数传入。例外:当ServiceItem需要在多个地方使用,而且要叠加很多不一样的样式,原写法会方便些。

// 不好的写法
<div key={item.uuid} className={scss.serviceItemContainer}>
    <ServiceItem item={item} />
</div>

// 好的写法
<ServiceItem key={item.uuid} item={item} className={customStyle} />

10 如何处理 this

因为函数组件不需要 this 绑定,所以只要有可能就要使用它们。 但是如果正在使用 ES6类,需要手动绑定这个类,因为 React 不能自动绑定该组件中的函数。

10.1 渲染时绑定

class Foo extends Components {
 constructor(props) {
   super(props);
   this.state = { message: "Hello" };
 }
 logMessage() {
   const { message } = this.state;
   console.log(message);
 }
 render() {
   return (
     <input type="button" value="Log" onClick={this.logMessage.bind(this)} />
   );
 }
}

上面的函数的 this 绑定如下:

onClick={this.logMessage.bind(this)}

这种方法清晰、简洁、有效,但是它可能会导致一个轻微的性能问题,因为每次此组件 re-rendered 时都会频繁的调用一个新的 logMessage 函数

10.2 render 函数中的箭头函数

class Bar extends Components {
  constructor(props) {
    super(props);
    this.state = { message: "Hello" };
  }
  logMessage() {
    const { message } = this.state;
    console.log(message);
  }
  render() {
    return (
      <input type="button" value="Log" onClick={() => this.logMessage()} />
    );
  }
}

上面的函数的 this 绑定如下:

onClick={() => this.logMessage()}

这种方法非常简洁,就像例子10.1,但是和例子10.1一样,它也会在每次 render 这个组件时创建一个新的 logMessage 函数。

10.3 构造函数中绑定this

class Hello extends Components {
 constructor(props) {
   super(props);
   this.state = { message: "Hello" };
   this.logMessage = this.logMessage.bind(this); 
 }
 logMessage() {
   const { message } = this.state;
   console.log(message);
 }
 render() {
   return (
     <input type="button" value="Log" onClick={this.logMessage} />
   );
 }
}

上面的函数的 this 绑定如下:

this.logMessage = this.logMessage.bind(this);

这种方法将解决示例1和2的潜在性能问题。 但是不要忘记在构造函数中调用 super

constructor中通常只做两件事情:

  1. 通过给 this.state 赋值对象来初始化内部的state;
  2. 为事件绑定实例(this)

10.4 Class 属性中的箭头函数

class Message extends Components {
 constructor(props) {
   super(props);
   this.state = { message: "Hello" }; 
 }
 logMessage = () => {
   const { message } = this.state;
   console.log(message);
 }
 render() {
   return (
     <input type="button" value="Log" onClick={this.logMessage} />
   );
 }
}

上面的函数的 this 绑定如下:

logMessage = () => {
  const { message } = this.state;
  console.log(message);
}

这种方式非常干净,可读性强,可以避免示例1和示例2的性能问题,并避免示例3中的重复。

举报

相关推荐

0 条评论