3

React组件设计

组件分类

展示组件和容器组件

展示组件 容器组件
关注事物的展示 关注事物如何工作
可能包含展示和容器组件,并且一般会有DOM标签和css样式 可能包含展示和容器组件,并且不会有DOM标签和css样式
常常允许通过this.props.children传递 提供数据和行为给容器组件或者展示组件
对第三方没有任何依赖,比如store 或者 flux action 调用flux action 并且提供他们的回调给展示组件
不要指定数据如何加载和变化 作为数据源,通常采用较高阶的组件,而不是自己写,比如React Reduxconnect(), Relay的createContainer(), Flux UtilsContainer.create()
仅通过属性获取数据和回调 null
很少有自己的状态,即使有,也是自己的UI状态 null
除非他们需要的自己的状态,生命周期,或性能优化才会被写为功能组件 null

下面是一个可能会经常写的组件,评论列表组件,数据交互和展示都放到了一个组件里面。

// CommentList.js
class CommentList extends React.Component {
  constructor() {
    super();
    this.state = { comments: [] }
  }
  componentDidMount() {
    $.ajax({
      url: "/my-comments.json",
      dataType: 'json',
      success: function(comments) {
        this.setState({comments: comments});
      }.bind(this)
    });
  }
  render() {
    return <ul> {this.state.comments.map(renderComment)} </ul>;
  }
  renderComment({body, author}) {
    return <li>{body}—{author}</li>;
  }
}

我们对上面的组件进行拆分,把他拆分成容器组件 CommentListContainer.js 和展示组件 CommentList

// CommentListContainer.js
class CommentListContainer extends React.Component {
  constructor() {
    super();
    this.state = { comments: [] }
  }
  componentDidMount() {
    $.ajax({
      url: "/my-comments.json",
      dataType: 'json',
      success: function(comments) {
        this.setState({comments: comments});
      }.bind(this)
    });
  }
  render() {
    return <CommentList comments={this.state.comments} />;
  }
}


// CommentList.js
class CommentList extends React.Component {
  constructor(props) {
    super(props);
  }
  render() { 
    return <ul> {this.props.comments.map(renderComment)} </ul>;
  }
  renderComment({body, author}) {
    return <li>{body}—{author}</li>;
  }
}

优势:

  • 展示和容器更好的分离,更好的理解应用程序和UI
  • 重用性高,展示组件可以用于多个不同的state数据源
  • 展示组件就是你的调色板,可以把他们放到单独的页面,在不影响应用程序的情况下,让设计师调整UI
  • 迫使你分离标签,达到更高的可用性

有状态组件和无状态组件

下面是一个最简单的无状态组件的例子:

function HelloComponent(props, /* context */) {
  return <div>Hello {props.name}</div>
}
ReactDOM.render(<HelloComponent name="Sebastian" />, mountNode)

可以看到,原本需要写“类”定义(React.createClass 或者 class YourComponent extends React.Component)来创建自己组件的定义(有状态组件),现在被精简成了只写一个 render 函数。更值得一提的是,由于仅仅是一个无状态函数,React 在渲染的时候也省掉了将“组件类” 实例化的过程。

结合 ES6 的解构赋值,可以让代码更精简。例如下面这个 Input 组件:

function Input({ label, name, value, ...props }, { defaultTheme }) {
  const { theme, autoFocus, ...rootProps } = props
  return (
    <label
      htmlFor={name}
      children={label || defaultLabel}
      {...rootProps}
    >
    <input
      name={name}
      type="text"
      value={value || ''}
      theme={theme || defaultTheme}
      {...props}
    />
  )}
Input.contextTypes = {defaultTheme: React.PropTypes.object};

无状态组件不像上述两种方法在调用时会创建新实例,它创建时始终保持了一个实例,避免了不必要的检查和内存分配,做到了内部优化。

无状态组件不支持 "ref"

高阶组件

高阶组件通过函数和闭包,改变已有组件的行为,本质上就是 Decorator 模式在 React 的一种实现。

当写着写着无状态组件的时候,有一天忽然发现需要状态处理了,那么无需彻底返工:)
往往我们需要状态的时候,这个需求是可以重用的。

高阶组件加无状态组件,则大大增强了整个代码的可测试性和可维护性。同时不断“诱使”我们写出组合性更好的代码。

高阶函数
function welcome() {
    let username = localStorage.getItem('username');
    console.log('welcome ' + username);
}

function goodbey() {
    let username = localStorage.getItem('username');
    console.log('goodbey ' + username);
}

welcome();
goodbey();

我们发现两个函数有一句代码是一样的,这叫冗余唉。(平时可能会有一大段代码的冗余)。

下面我们要写一个中间函数,读取username,他来负责把username传递给两个函数。

function welcome(username) {
    console.log('welcome ' + username);
}

function goodbey(username) {
    console.log('goodbey ' + username);
}

function wrapWithUsername(wrappedFunc) {
    let newFunc = () => {
        let username = localStorage.getItem('username');
        wrappedFunc(username);
    };
    return newFunc;
}

welcome = wrapWithUsername(welcome);
goodbey = wrapWithUsername(goodbey);

welcome();
goodbey();

好了,我们里面的 wrapWithUsername 函数就是一个“高阶函数”。
他做了什么?他帮我们处理了 username,传递给目标函数。我们调用最终的函数 welcome的时候,根本不用关心 username是怎么来的。

举一反三的高阶组件

下面是两个冗余的组件。

import React, {Component} from 'react'

class Welcome extends Component {
    constructor(props) {
        super(props);
        this.state = {
            username: ''
        }
    }

    componentWillMount() {
        let username = localStorage.getItem('username');
        this.setState({
            username: username
        })
    }

    render() {
        return (
            <div>welcome {this.state.username}</div>
        )
    }
}

export default Welcome;
import React, {Component} from 'react'

class Goodbye extends Component {
    constructor(props) {
        super(props);
        this.state = {
            username: ''
        }
    }

    componentWillMount() {
        let username = localStorage.getItem('username');
        this.setState({
            username: username
        })
    }

    render() {
        return (
            <div>goodbye {this.state.username}</div>
        )
    }
}

export default Goodbye;

我们可以通过刚刚高阶函数的思想来创建一个中间组件,也就是我们说的高阶组件。

import React, {Component} from 'react'

export default (WrappedComponent) => {
    class NewComponent extends Component {
        constructor() {
            super();
            this.state = {
                username: ''
            }
        }

        componentWillMount() {
            let username = localStorage.getItem('username');
            this.setState({
                username: username
            })
        }

        render() {
            return <WrappedComponent username={this.state.username}/>
        }
    }

    return NewComponent
}
import React, {Component} from 'react';
import wrapWithUsername from 'wrapWithUsername';

class Welcome extends Component {

    render() {
        return (
            <div>welcome {this.props.username}</div>
        )
    }
}

Welcome = wrapWithUsername(Welcome);

export default Welcome;
import React, {Component} from 'react';
import wrapWithUsername from 'wrapWithUsername';

class Goodbye extends Component {

    render() {
        return (
            <div>goodbye {this.props.username}</div>
        )
    }
}

Goodbye = wrapWithUsername(Goodbye);

export default Goodbye;

看到没有,高阶组件就是把 username 通过 props 传递给目标组件了。目标组件只管从 props里面拿来用就好了。

为了代码的复用性,我们应该尽量减少代码的冗余。

  1. 提取共享的state,如果有两个组件都需要加载同样的数据,那么他们会有相同的 componentDidMount 函数。
  2. 找出重复的代码,每个组件中constructor 和 componentDidMount都干着同样的事情,另外,在数据拉取时都会显示Loading... 文案,那么我们应该思考如何使用高阶组件来提取这些方法。
  3. 迁移重复的代码到高阶组件
  4. 包裹组件,并且使用props替换state
  5. 尽可能地简化

组件开发基本思想

单功能原则

使用react时,组件或容器的代码在根本上必须只负责一块UI功能。

让组件保持简单

  • 如果组件根本不需要状态,那么就使用函数定义的无状态组件。
  • 从性能上来说,函数定义的无状态组件 > ES6 class 定义的组件 > 通过 React.createClass() 定义的组件。
  • 仅传递组件所需要的属性。只有当属性列表太长时,才使用{...this.props}进行传递。
  • 如果组件里面有太多的判断逻辑(if-else语句)通常意味着这个组件需要被拆分成更细的组件或模块。
  • 使用明确的命名能够让开发者明白它的功能,有助于组件复用。

基本准则

  • shouldComponentUpdate中避免不必要的检查.
  • 尽量使用不可变数据类型(Immutable).
  • 编写针对产品环境的打包配置(Production Build).
  • 通过Chrome Timeline来记录组件所耗费的资源.
  • componentWillMount或者componentDidMount里面通过setTimeOut或者requestAnimationFram来延迟执行那些需要大量计算的任务.

组件开发技巧

form表单里的受控组件和不受控组件

受控组件

在大多数情况下,我们推荐使用受控组件来实现表单。在受控组件中,表单数据由 React 组件负责处理。下面是一个典型的受控组建。

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

设置表单元素的value属性之后,其显示值将由this.state.value决定,以满足React状态的同一数据理念。每次键盘敲击之后会执行handleChange方法以更新React状态,显示值也将随着用户的输入改变。

对于受控组件来说,每一次 state(状态)变化都会伴有相关联的处理函数。这使得可以直接修改或验证用户的输入和提交表单。

不受控组件

因为不受控组件的数据来源是 DOM 元素,当使用不受控组件时很容易实现 React 代码与非 React 代码的集成。如果你希望的是快速开发、不要求代码质量,不受控组件可以一定程度上减少代码量。否则。你应该使用受控组件。

一般情况下不受控组件我们使用ref来获取DOM元素进行操作。

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.input.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={(input) => this.input = input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

组件条件判断

三元函数组件判断渲染
const sampleComponent = () => {
  return isTrue ? <p>True!</p> : <p>false!</p>
};

使用&&表达式替换不必要的三元函数

const sampleComponent = () => {
  return isTrue ? <p>True!</p> : <none/>
};
const sampleComponent = () => {
  return isTrue && <p>True!</p>
};

需要注意的是如果isTrue 为 0 ,其实会转换成 false,但是在页面中显示的时候,&&还是会返回0显示到页面中。

多重嵌套判断

// 问题代码
const sampleComponent = () => {
  return (
    <div>
      {flag && flag2 && !flag3
        ? flag4
        ? <p>Blah</p>
        : flag5
        ? <p>Meh</p>
        : <p>Herp</p>
        : <p>Derp</p>
      }
    </div>
  )
};

解决方案:

  • 最佳方案: 将逻辑移到子组件内部
  • 使用IIFE(Immediately-Invoked Function Expression 立即执行函数)
  • 满足条件的时候使用return强制跳出函数
const sampleComponent = () => {
  const basicCondition = flag && flag2 && !flag3;
  if (!basicCondition) return <p>Derp</p>;
  if (flag4) return <p>Blah</p>;
  if (flag5) return <p>Meh</p>;
  return <p>Herp</p>
}

setState异步性

在某些情况下,React框架出于性能优化考虑,可能会将多次state更新合并成一次更新。正因为如此,setState实际上是一个异步的函数。 如果在调用setState()函数之后尝试去访问this.state,你得到的可能还是setState()函数执行之前的结果。

但是,有一些行为也会阻止React框架本身对于多次state更新的合并,从而让state的更新变得同步化。 比如: eventListeners, Ajax, setTimeout 等等。

React框架之所以在选择在调用setState函数之后立即更新state而不是采用框架默认的方式,即合并多次state更新为一次更新,是因为这些函数调用(fetch,setTimeout等浏览器层面的API调用)并不处于React框架的上下文中,React没有办法对其进行控制。React在此时采用的策略就是及时更新,确保在这些函数执行之后的其他代码能拿到正确的数据(即更新过的state)。

解决setState函数异步的办法?

根据React官方文档,setState函数实际上接收两个参数,其中第二个参数类型是一个函数,作为setState函数执行后的回调。通过传入回调函数的方式,React可以保证传入的回调函数一定是在setState成功更新this.state之后再执行。

this.setState({count: 1}, () => {
    console.log(this.state.count); // 1
})
React源码中setState的实现
ReactComponent.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
    typeof partialState === 'function' ||
    partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
    'function which returns an object of state variables.'
  );
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

updater的这两个方法,和React底层的Virtual Dom(虚拟DOM树)的diff算法有紧密的关系,所以真正决定同步还是异步的其实是Virtual DOMdiff算法。

依赖注入

React中,想做依赖注入(Dependency Injection)其实相当简单。可以通过props来进行传递。但是,如果组件数量很多,并且组件嵌套层次很深的话,这种方式就不太合适。

高阶组件
// inject.jsx
var title = 'React Dependency Injection';
export default function inject(Component) {
  return class Injector extends React.Component {
    render() {
      return (
        <Component
          {...this.state}
          {...this.props}
          title={ title }
        />
      )
    }
  };
}
// Title.jsx
export default function Title(props) {
  return <h1>{ props.title }</h1>;
}
// Header.jsx
import inject from './inject.jsx';
import Title from './Title.jsx';

var EnhancedTitle = inject(Title);
export default function Header() {
  return (
    <header>
      <EnhancedTitle />
    </header>
  );
}
context

React v16.3.0 之前的 Context:

var context = { title: 'React in patterns' };
class App extends React.Component {
  getChildContext() {
    return context;
  }
  // ...
}

App.childContextTypes = {
  title: PropTypes.string
};
class Inject extends React.Component {
  render() {
    var title = this.context.title;
  // ...
  }
}
Inject.contextTypes = {
  title: PropTypes.string
};

之前的 Context 作为一个实验性质的 API,直到 React v16.3.0 版本前都一直不被官方所提倡去使用,其主要原因就是因为在子组件中使用 Context 会破坏 React 应用的分型架构。

这里的分形架构指的是从理想的 React 应用的根组件树中抽取的任意一部分都仍是一个可以直接运行的子组件树。在这个子组件树之上再包一层,就可以将它无缝地移植到任意一个其他的根组件树中。

但如果根组件树中有任意一个组件使用了支持透传的 Context API,那么如果把包含了这个组件的子组件树单独拿出来,因为缺少了提供 Context 值的根组件树,这时的这个子组件树是无法直接运行的。

并且他有一个致命缺陷:任何一个中间传递的组件shouldComponentUpdate 函数返回false,组件都不会得到更新。

新的Context Api

新的Context Api 采用声明式的写法,并且可以透过shouldComponentUpdate 函数返回false的组件继续向下传播,以保证目标组件一定可以接收到顶层组件 Context 值的更新,一举解决了现有 Context API 的两大弊端,也终于成为了 React 中的第一级(first-class) API

新的 Context API 分为三个组成部分:

  1. React.createContext 用于初始化一个 Context
  2. XXXContext.Provider作为顶层组件接收一个名为 valueprop,可以接收任意需要被放入 Context 中的字符串,数字,甚至是函数。
  3. XXXContext.Consumer作为目标组件可以出现在组件树的任意位置(在 Provider 之后),接收 children prop,这里的 children 必须是一个函数(context =&gt; ())用来接收从顶层传来的 Context
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton(props) {
  return (
    <ThemeContext.Consumer>
      {theme => <Button {...props} theme={theme} />}
    </ThemeContext.Consumer>
  );
}

事件处理中的this指向问题

class Switcher extends React.Component {
  constructor(props) {
    super(props);
    this.state = { name: 'React in patterns' };
  }
  render() {
    return (
      <button onClick={ this._handleButtonClick }>
        click me
      </button>
    );
  }

  _handleButtonClick() {
    console.log(`Button is clicked inside ${ this.state.name }`);
    // 将导致
    // Uncaught TypeError: Cannot read property 'state' of null
  }
}

我们可以通过下面三种方式简单实现this指向的绑定:

  • constructor 中事先绑定 this._buttonClick = this._handleButtonClick.bind(this);
  • 调用时使用箭头函数 <button onClick={ () => this._buttonClick() }>
  • ES7中的绑定操作符 <button onClick={ ::this._buttonClick() }>

给setState传入回调函数

setState() 不仅能接受一个对象,还能接受一个函数作为参数呢,该函数接受该组件前一刻的 state 以及当前的 props 作为参数,计算和返回下一刻的 state。

// assuming this.state.count === 0
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
// this.state.count === 1, not 3

this.setState((prevState, props) => ({
  count: prevState.count + props.increment
}));
// Passing object
this.setState({ expanded: !this.state.expanded });

// Passing function
this.setState(prevState => ({ expanded: !prevState.expanded }));

组件切换技巧

import HomePage from './HomePage.jsx';
import AboutPage from './AboutPage.jsx';
import UserPage from './UserPage.jsx';
import FourOhFourPage from './FourOhFourPage.jsx';

const PAGES = {
  home: HomePage,
  about: AboutPage,
  user: UserPage
};

const Page = (props) => {
  const Handler = PAGES[props.page] || FourOhFourPage;

  return <Handler {...props} />
};

React style

组件分类

基础组件, 布局组件, 排版组件

给无状态的纯UI组件应用样式

请保持样式远离那些离不开state的组件. 比如路由, 视图, 容器, 表单, 布局等等不应该有任何的样式或者css class出现在组件上. 相反, 这些复杂的业务组件应该有一些带有基本功能的无状态UI组件组成.

class SampleComponent extends Component {
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <Heading children='Sign In'/>
        <Input
          name='username'
          value={username}
          onChange={this.handleChange}/>
        <Input
          type='password'
          name='password'
          value={password}
          onChange={this.handleChange}/>
        <Button
          type='submit'
          children='Sign In'/>
      </form>
    )
  }
}

// 表达组件(带样式)
const Button = ({
  ...props
  }) => {
  const sx = {
    fontFamily: 'inherit',
    fontSize: 'inherit',
    fontWeight: 'bold',
    textDecoration: 'none',
    display: 'inline-block',
    margin: 0,
    paddingTop: 8,
    paddingBottom: 8,
    paddingLeft: 16,
    paddingRight: 16,
    border: 0,
    color: 'white',
    backgroundColor: 'blue',
    WebkitAppearance: 'none',
    MozAppearance: 'none'
  }

  return (
    <button {...props} style={sx}/>
  )
}

样式模块(style module)

一般来说, 在组件内写死(hard code)样式应该是要被避免的. 这些有可能被不同的UI组件分享的样式应该被分开放入对应的模块中.

// 样式模块
export const white = '#fff';
export const black = '#111';
export const blue = '#07c';

export const colors = {
  white,
  black,
  blue
};

export const space = [
  0,
  8,
  16,
  32,
  64
];

const styles = {
  bold: 600,
  space,
  colors
};

export default styles
// button.jsx
import React from 'react'
import { bold, space, colors } from './styles'

const Button = ({
  ...props
  }) => {
  const sx = {
    fontFamily: 'inherit',
    fontSize: 'inherit',
    fontWeight: bold,
    textDecoration: 'none',
    display: 'inline-block',
    margin: 0,
    paddingTop: space[1],
    paddingBottom: space[1],
    paddingLeft: space[2],
    paddingRight: space[2],
    border: 0,
    color: colors.white,
    backgroundColor: colors.blue,
    WebkitAppearance: 'none',
    MozAppearance: 'none'
  };

  return (
    <button {...props} style={sx}/>
  )
};

样式函数(Style Functions)

// Modular powers of two scale
const scale = [
  0,
  8,
  16,
  32,
  64
];

// 通过这个函数去取得一部分的样式
const createScaledPropertyGetter = (scale) => (prop) => (x) => {
  return (typeof x === 'number' && typeof scale[x] === 'number')
    ? {[prop]: scale[x]}
    : null
};
const getScaledProperty = createScaledPropertyGetter(scale);

export const getMargin = getScaledProperty('margin');
export const getPadding = getScaledProperty('padding');
// 样式函数的用法
const Box = ({
  m,
  p,
  ...props
  }) => {
  const sx = {
    ...getMargin(m),
    ...getPadding(p)
  };

  return <div {...props} style={sx}/>
};

// 组件用法.
const Box = () => (
  <div>
    <Box m={2} p={3}>
      A box with 16px margin and 32px padding
    </Box>
  </div>
);

常见小坑

state不更新?

class SampleComponent extends Component {
  // constructor function (or getInitialState)
  constructor(props) {
    super(props);
    this.state = {
      flag: false,
      inputVal: props.inputValue
    };
  }

  render() {
    return <div>{this.state.inputVal && <AnotherComponent/>}</div>
  }
}

这样做的危险在于, 有可能组件的props发生了改变但是组件却没有被更新. 新的props的值不会被React认为是更新的数据因为构造器constructor或者getInitialState方法在组件创建之后不会再次被调用了,因此组件的state不再会被更新。 要记住, State的初始化只会在组件第一次初始化的时候发生。

class SampleComponent extends Component {
  // constructor function (or getInitialState)
  constructor(props) {
    super(props);
    this.state = {
      flag: false
    };
  }

  render() {
    return <div>{this.props.inputValue && <AnotherComponent/>}</div>
  }
}

更干净的render函数?

更干净的render函数? 这个概念可能会有点让人疑惑.

其实在这里干净是指我们在shouldComponentUpdate这个生命周期函数里面去做浅比较, 从而避免不必要的渲染.

class Table extends PureComponent {
  render() {
    return (
      <div>
        {this.props.items.map(i =>
          <Cell data={i} options={this.props.options || []}/>
        )}
      </div>
    );
  }
}

这种写法的问题在于{this.props.options || []} 这种写法会导致所有的Cell都被重新渲染即使只有一个cell发生了改变. 为什么会发生这种事呢?

仔细观察你会发现, options这个数组被传到了Cell这个组件上, 一般情况下, 这不会导致什么问题. 因为如果有其他的Cell组件, 组件会在有props发生改变的时候浅对比props并且跳过渲染(因为对于其他Cell组件, props并没有发生改变). 但是在这个例子里面, 当optionsnull时, 一个默认的空数组就会被当成Props传到组件里面去. 事实上每次传入的[]都相当于创建了新的Array实例. 在JavaScript里面, 不同的实例是有不同的实体的, 所以浅比较在这种情况下总是会返回false, 然后组件就会被重新渲染. 因为两个实体不是同一个实体. 这就完全破坏了React对于我们组件渲染的优化.

const defaultval = [];  // <---  也可以使用defaultProps
class Table extends PureComponent {
  render() {
    return (
      <div>
        {this.props.items.map(i =>
          <Cell data={i} options={this.props.options || defaultval}/>
        )}
      </div>
    );
  }
}

还是多次重新渲染

class App extends PureComponent {
  render() {
    return <MyInput
      onChange={e => this.props.update(e.target.value)}/>;
  }
}
class App extends PureComponent {
  update(e) {
    this.props.update(e.target.value);
  }

  render() {
    return <MyInput onChange={this.update.bind(this)}/>;
  }
}

在上面的两个坏实践中, 每次我们都会去创建一个新的函数实体. 和第一个例子类似, 新的函数实体会让我们的浅比较返回false, 导致组件被重新渲染. 所以我们需要在更早的时候去bind我们的函数.

class App extends PureComponent {
  constructor(props) {
    super(props);
    this.update = this.update.bind(this);
  }

  update(e) {
    this.props.update(e.target.value);
  }

  render() {
    return <MyInput onChange={this.update}/>;
  }
}

命名

引用命名

React模块名使用帕斯卡命名,实例使用骆驼式命名

// bad
import reservationCard from './ReservationCard';

// good
import ReservationCard from './ReservationCard';

// bad
const ReservationItem = <ReservationCard />;

// good
const reservationItem = <ReservationCard />;

高阶模块命名

// bad
export default function withFoo(WrappedComponent) {
  return function WithFoo(props) {
    return <WrappedComponent {...props} foo />;
  }
}

// good
export default function withFoo(WrappedComponent) {
  function WithFoo(props) {
    return <WrappedComponent {...props} foo />;
  }

  const wrappedComponentName = WrappedComponent.displayName
    || WrappedComponent.name
    || 'Component';

  WithFoo.displayName = `withFoo(${wrappedComponentName})`;
  return WithFoo;
}

属性命名

避免使用DOM相关的属性来用作其他的用途。

// bad
<MyComponent style="fancy" />

// good
<MyComponent variant="fancy" />

私有函数添加 _ 前缀?

在React模块中,不要给所谓的私有函数添加 _ 前缀,本质上它并不是私有的。

为什么?_ 下划线前缀在某些语言中通常被用来表示私有变量或者函数。但是不像其他的一些语言,在JS中没有原生支持所谓的私有变量,所有的变量函数都是共有的。尽管你的意图是使它私有化,在之前加上下划线并不会使这些变量私有化,并且所有的属性(包括有下划线前缀及没有前缀的)都应该被视为是共有的。

Ordering React 模块生命周期

class extends React.Component 的生命周期函数:
可选的 static 方法

  • constructor 构造函数
  • getChildContext 获取子元素内容
  • componentWillMount 模块渲染前
  • componentDidMount 模块渲染后
  • componentWillReceiveProps 模块将接受新的数据
  • shouldComponentUpdate 判断模块需不需要重新渲染
  • componentWillUpdate 上面的方法返回 true, 模块将重新渲染
  • componentDidUpdate 模块渲染结束
  • componentWillUnmount 模块将从DOM中清除, 做一些清理任务

点击回调或者事件处理器 如 onClickSubmit()onChangeDescription()

render 里的 getter 方法 如 getSelectReason()getFooterContent()

可选的 render 方法 如 renderNavigation()renderProfilePicture()

render render() 方法

如何定义 propTypes, defaultProps, contextTypes, 等等其他属性...

import React from 'react';
import PropTypes from 'prop-types';

const propTypes = {
  id: PropTypes.number.isRequired,
  url: PropTypes.string.isRequired,
  text: PropTypes.string,
};

const defaultProps = {
  text: 'Hello World',
};

class Link extends React.Component {
  static methodsAreOk() {
    return true;
  }

  render() {
    return <a href={this.props.url} data-id={this.props.id}>{this.props.text}</a>;
  }
}

Link.propTypes = propTypes;
Link.defaultProps = defaultProps;

export default Link;

楼兰小骑士
143 声望9 粉丝

自由