React组件设计

组件分类

展示组件和容器组件

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

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

  1. // CommentList.js
  2. class CommentList extends React.Component {
  3. constructor() {
  4. super();
  5. this.state = { comments: [] }
  6. }
  7. componentDidMount() {
  8. $.ajax({
  9. url: "/my-comments.json",
  10. dataType: 'json',
  11. success: function(comments) {
  12. this.setState({comments: comments});
  13. }.bind(this)
  14. });
  15. }
  16. render() {
  17. return <ul> {this.state.comments.map(renderComment)} </ul>;
  18. }
  19. renderComment({body, author}) {
  20. return <li>{body}—{author}</li>;
  21. }
  22. }

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

  1. // CommentListContainer.js
  2. class CommentListContainer extends React.Component {
  3. constructor() {
  4. super();
  5. this.state = { comments: [] }
  6. }
  7. componentDidMount() {
  8. $.ajax({
  9. url: "/my-comments.json",
  10. dataType: 'json',
  11. success: function(comments) {
  12. this.setState({comments: comments});
  13. }.bind(this)
  14. });
  15. }
  16. render() {
  17. return <CommentList comments={this.state.comments} />;
  18. }
  19. }
  20. // CommentList.js
  21. class CommentList extends React.Component {
  22. constructor(props) {
  23. super(props);
  24. }
  25. render() {
  26. return <ul> {this.props.comments.map(renderComment)} </ul>;
  27. }
  28. renderComment({body, author}) {
  29. return <li>{body}—{author}</li>;
  30. }
  31. }

优势:

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

有状态组件和无状态组件

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

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

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

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

  1. function Input({ label, name, value, ...props }, { defaultTheme }) {
  2. const { theme, autoFocus, ...rootProps } = props
  3. return (
  4. <label
  5. htmlFor={name}
  6. children={label || defaultLabel}
  7. {...rootProps}
  8. >
  9. <input
  10. name={name}
  11. type="text"
  12. value={value || ''}
  13. theme={theme || defaultTheme}
  14. {...props}
  15. />
  16. )}
  17. Input.contextTypes = {defaultTheme: React.PropTypes.object};

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

无状态组件不支持 "ref"

高阶组件

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

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

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

高阶函数

  1. function welcome() {
  2. let username = localStorage.getItem('username');
  3. console.log('welcome ' + username);
  4. }
  5. function goodbey() {
  6. let username = localStorage.getItem('username');
  7. console.log('goodbey ' + username);
  8. }
  9. welcome();
  10. goodbey();

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

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

  1. function welcome(username) {
  2. console.log('welcome ' + username);
  3. }
  4. function goodbey(username) {
  5. console.log('goodbey ' + username);
  6. }
  7. function wrapWithUsername(wrappedFunc) {
  8. let newFunc = () => {
  9. let username = localStorage.getItem('username');
  10. wrappedFunc(username);
  11. };
  12. return newFunc;
  13. }
  14. welcome = wrapWithUsername(welcome);
  15. goodbey = wrapWithUsername(goodbey);
  16. welcome();
  17. goodbey();

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

举一反三的高阶组件

下面是两个冗余的组件。

  1. import React, {Component} from 'react'
  2. class Welcome extends Component {
  3. constructor(props) {
  4. super(props);
  5. this.state = {
  6. username: ''
  7. }
  8. }
  9. componentWillMount() {
  10. let username = localStorage.getItem('username');
  11. this.setState({
  12. username: username
  13. })
  14. }
  15. render() {
  16. return (
  17. <div>welcome {this.state.username}</div>
  18. )
  19. }
  20. }
  21. export default Welcome;
  1. import React, {Component} from 'react'
  2. class Goodbye extends Component {
  3. constructor(props) {
  4. super(props);
  5. this.state = {
  6. username: ''
  7. }
  8. }
  9. componentWillMount() {
  10. let username = localStorage.getItem('username');
  11. this.setState({
  12. username: username
  13. })
  14. }
  15. render() {
  16. return (
  17. <div>goodbye {this.state.username}</div>
  18. )
  19. }
  20. }
  21. export default Goodbye;

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

  1. import React, {Component} from 'react'
  2. export default (WrappedComponent) => {
  3. class NewComponent extends Component {
  4. constructor() {
  5. super();
  6. this.state = {
  7. username: ''
  8. }
  9. }
  10. componentWillMount() {
  11. let username = localStorage.getItem('username');
  12. this.setState({
  13. username: username
  14. })
  15. }
  16. render() {
  17. return <WrappedComponent username={this.state.username}/>
  18. }
  19. }
  20. return NewComponent
  21. }
  1. import React, {Component} from 'react';
  2. import wrapWithUsername from 'wrapWithUsername';
  3. class Welcome extends Component {
  4. render() {
  5. return (
  6. <div>welcome {this.props.username}</div>
  7. )
  8. }
  9. }
  10. Welcome = wrapWithUsername(Welcome);
  11. export default Welcome;
  1. import React, {Component} from 'react';
  2. import wrapWithUsername from 'wrapWithUsername';
  3. class Goodbye extends Component {
  4. render() {
  5. return (
  6. <div>goodbye {this.props.username}</div>
  7. )
  8. }
  9. }
  10. Goodbye = wrapWithUsername(Goodbye);
  11. 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 组件负责处理。下面是一个典型的受控组建。

  1. class NameForm extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {value: ''};
  5. this.handleChange = this.handleChange.bind(this);
  6. this.handleSubmit = this.handleSubmit.bind(this);
  7. }
  8. handleChange(event) {
  9. this.setState({value: event.target.value});
  10. }
  11. handleSubmit(event) {
  12. alert('A name was submitted: ' + this.state.value);
  13. event.preventDefault();
  14. }
  15. render() {
  16. return (
  17. <form onSubmit={this.handleSubmit}>
  18. <label>
  19. <input type="text" value={this.state.value} onChange={this.handleChange} />
  20. </label>
  21. <input type="submit" value="Submit" />
  22. </form>
  23. );
  24. }
  25. }

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

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

不受控组件

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

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

  1. class NameForm extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.handleSubmit = this.handleSubmit.bind(this);
  5. }
  6. handleSubmit(event) {
  7. alert('A name was submitted: ' + this.input.value);
  8. event.preventDefault();
  9. }
  10. render() {
  11. return (
  12. <form onSubmit={this.handleSubmit}>
  13. <label>
  14. Name:
  15. <input type="text" ref={(input) => this.input = input} />
  16. </label>
  17. <input type="submit" value="Submit" />
  18. </form>
  19. );
  20. }
  21. }

组件条件判断

三元函数组件判断渲染

  1. const sampleComponent = () => {
  2. return isTrue ? <p>True!</p> : <p>false!</p>
  3. };

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

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

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

多重嵌套判断

  1. // 问题代码
  2. const sampleComponent = () => {
  3. return (
  4. <div>
  5. {flag && flag2 && !flag3
  6. ? flag4
  7. ? <p>Blah</p>
  8. : flag5
  9. ? <p>Meh</p>
  10. : <p>Herp</p>
  11. : <p>Derp</p>
  12. }
  13. </div>
  14. )
  15. };

解决方案:

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

setState异步性

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

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

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

解决setState函数异步的办法?

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

  1. this.setState({count: 1}, () => {
  2. console.log(this.state.count); // 1
  3. })

React源码中setState的实现

  1. ReactComponent.prototype.setState = function(partialState, callback) {
  2. invariant(
  3. typeof partialState === 'object' ||
  4. typeof partialState === 'function' ||
  5. partialState == null,
  6. 'setState(...): takes an object of state variables to update or a ' +
  7. 'function which returns an object of state variables.'
  8. );
  9. this.updater.enqueueSetState(this, partialState);
  10. if (callback) {
  11. this.updater.enqueueCallback(this, callback, 'setState');
  12. }
  13. };

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

依赖注入

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

高阶组件

  1. // inject.jsx
  2. var title = 'React Dependency Injection';
  3. export default function inject(Component) {
  4. return class Injector extends React.Component {
  5. render() {
  6. return (
  7. <Component
  8. {...this.state}
  9. {...this.props}
  10. title={ title }
  11. />
  12. )
  13. }
  14. };
  15. }
  1. // Title.jsx
  2. export default function Title(props) {
  3. return <h1>{ props.title }</h1>;
  4. }
  1. // Header.jsx
  2. import inject from './inject.jsx';
  3. import Title from './Title.jsx';
  4. var EnhancedTitle = inject(Title);
  5. export default function Header() {
  6. return (
  7. <header>
  8. <EnhancedTitle />
  9. </header>
  10. );
  11. }

context

React v16.3.0 之前的 Context:

  1. var context = { title: 'React in patterns' };
  2. class App extends React.Component {
  3. getChildContext() {
  4. return context;
  5. }
  6. // ...
  7. }
  8. App.childContextTypes = {
  9. title: PropTypes.string
  10. };
  1. class Inject extends React.Component {
  2. render() {
  3. var title = this.context.title;
  4. // ...
  5. }
  6. }
  7. Inject.contextTypes = {
  8. title: PropTypes.string
  9. };

之前的 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作为顶层组件接收一个名为 value的 prop,可以接收任意需要被放入 Context 中的字符串,数字,甚至是函数。

  3. XXXContext.Consumer作为目标组件可以出现在组件树的任意位置(在 Provider 之后),接收 children prop,这里的 children 必须是一个函数(context =&gt; ())用来接收从顶层传来的 Context

  1. const ThemeContext = React.createContext('light');
  2. class App extends React.Component {
  3. render() {
  4. return (
  5. <ThemeContext.Provider value="dark">
  6. <Toolbar />
  7. </ThemeContext.Provider>
  8. );
  9. }
  10. }
  11. function Toolbar(props) {
  12. return (
  13. <div>
  14. <ThemedButton />
  15. </div>
  16. );
  17. }
  18. function ThemedButton(props) {
  19. return (
  20. <ThemeContext.Consumer>
  21. {theme => <Button {...props} theme={theme} />}
  22. </ThemeContext.Consumer>
  23. );
  24. }

事件处理中的this指向问题

  1. class Switcher extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = { name: 'React in patterns' };
  5. }
  6. render() {
  7. return (
  8. <button onClick={ this._handleButtonClick }>
  9. click me
  10. </button>
  11. );
  12. }
  13. _handleButtonClick() {
  14. console.log(`Button is clicked inside ${ this.state.name }`);
  15. // 将导致
  16. // Uncaught TypeError: Cannot read property 'state' of null
  17. }
  18. }

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

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

给setState传入回调函数

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

  1. // assuming this.state.count === 0
  2. this.setState({count: this.state.count + 1});
  3. this.setState({count: this.state.count + 1});
  4. this.setState({count: this.state.count + 1});
  5. // this.state.count === 1, not 3
  6. this.setState((prevState, props) => ({
  7. count: prevState.count + props.increment
  8. }));
  1. // Passing object
  2. this.setState({ expanded: !this.state.expanded });
  3. // Passing function
  4. this.setState(prevState => ({ expanded: !prevState.expanded }));

组件切换技巧

  1. import HomePage from './HomePage.jsx';
  2. import AboutPage from './AboutPage.jsx';
  3. import UserPage from './UserPage.jsx';
  4. import FourOhFourPage from './FourOhFourPage.jsx';
  5. const PAGES = {
  6. home: HomePage,
  7. about: AboutPage,
  8. user: UserPage
  9. };
  10. const Page = (props) => {
  11. const Handler = PAGES[props.page] || FourOhFourPage;
  12. return <Handler {...props} />
  13. };

React style

组件分类

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

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

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

  1. class SampleComponent extends Component {
  2. render() {
  3. return (
  4. <form onSubmit={this.handleSubmit}>
  5. <Heading children='Sign In'/>
  6. <Input
  7. name='username'
  8. value={username}
  9. onChange={this.handleChange}/>
  10. <Input
  11. type='password'
  12. name='password'
  13. value={password}
  14. onChange={this.handleChange}/>
  15. <Button
  16. type='submit'
  17. children='Sign In'/>
  18. </form>
  19. )
  20. }
  21. }
  22. // 表达组件(带样式)
  23. const Button = ({
  24. ...props
  25. }) => {
  26. const sx = {
  27. fontFamily: 'inherit',
  28. fontSize: 'inherit',
  29. fontWeight: 'bold',
  30. textDecoration: 'none',
  31. display: 'inline-block',
  32. margin: 0,
  33. paddingTop: 8,
  34. paddingBottom: 8,
  35. paddingLeft: 16,
  36. paddingRight: 16,
  37. border: 0,
  38. color: 'white',
  39. backgroundColor: 'blue',
  40. WebkitAppearance: 'none',
  41. MozAppearance: 'none'
  42. }
  43. return (
  44. <button {...props} style={sx}/>
  45. )
  46. }

样式模块(style module)

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

  1. // 样式模块
  2. export const white = '#fff';
  3. export const black = '#111';
  4. export const blue = '#07c';
  5. export const colors = {
  6. white,
  7. black,
  8. blue
  9. };
  10. export const space = [
  11. 0,
  12. 8,
  13. 16,
  14. 32,
  15. 64
  16. ];
  17. const styles = {
  18. bold: 600,
  19. space,
  20. colors
  21. };
  22. export default styles
  1. // button.jsx
  2. import React from 'react'
  3. import { bold, space, colors } from './styles'
  4. const Button = ({
  5. ...props
  6. }) => {
  7. const sx = {
  8. fontFamily: 'inherit',
  9. fontSize: 'inherit',
  10. fontWeight: bold,
  11. textDecoration: 'none',
  12. display: 'inline-block',
  13. margin: 0,
  14. paddingTop: space[1],
  15. paddingBottom: space[1],
  16. paddingLeft: space[2],
  17. paddingRight: space[2],
  18. border: 0,
  19. color: colors.white,
  20. backgroundColor: colors.blue,
  21. WebkitAppearance: 'none',
  22. MozAppearance: 'none'
  23. };
  24. return (
  25. <button {...props} style={sx}/>
  26. )
  27. };

样式函数(Style Functions)

  1. // Modular powers of two scale
  2. const scale = [
  3. 0,
  4. 8,
  5. 16,
  6. 32,
  7. 64
  8. ];
  9. // 通过这个函数去取得一部分的样式
  10. const createScaledPropertyGetter = (scale) => (prop) => (x) => {
  11. return (typeof x === 'number' && typeof scale[x] === 'number')
  12. ? {[prop]: scale[x]}
  13. : null
  14. };
  15. const getScaledProperty = createScaledPropertyGetter(scale);
  16. export const getMargin = getScaledProperty('margin');
  17. export const getPadding = getScaledProperty('padding');
  18. // 样式函数的用法
  19. const Box = ({
  20. m,
  21. p,
  22. ...props
  23. }) => {
  24. const sx = {
  25. ...getMargin(m),
  26. ...getPadding(p)
  27. };
  28. return <div {...props} style={sx}/>
  29. };
  30. // 组件用法.
  31. const Box = () => (
  32. <div>
  33. <Box m={2} p={3}>
  34. A box with 16px margin and 32px padding
  35. </Box>
  36. </div>
  37. );

常见小坑

state不更新?

  1. class SampleComponent extends Component {
  2. // constructor function (or getInitialState)
  3. constructor(props) {
  4. super(props);
  5. this.state = {
  6. flag: false,
  7. inputVal: props.inputValue
  8. };
  9. }
  10. render() {
  11. return <div>{this.state.inputVal && <AnotherComponent/>}</div>
  12. }
  13. }

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

  1. class SampleComponent extends Component {
  2. // constructor function (or getInitialState)
  3. constructor(props) {
  4. super(props);
  5. this.state = {
  6. flag: false
  7. };
  8. }
  9. render() {
  10. return <div>{this.props.inputValue && <AnotherComponent/>}</div>
  11. }
  12. }

更干净的render函数?

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

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

  1. class Table extends PureComponent {
  2. render() {
  3. return (
  4. <div>
  5. {this.props.items.map(i =>
  6. <Cell data={i} options={this.props.options || []}/>
  7. )}
  8. </div>
  9. );
  10. }
  11. }

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

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

  1. const defaultval = []; // <--- 也可以使用defaultProps
  2. class Table extends PureComponent {
  3. render() {
  4. return (
  5. <div>
  6. {this.props.items.map(i =>
  7. <Cell data={i} options={this.props.options || defaultval}/>
  8. )}
  9. </div>
  10. );
  11. }
  12. }

还是多次重新渲染

  1. class App extends PureComponent {
  2. render() {
  3. return <MyInput
  4. onChange={e => this.props.update(e.target.value)}/>;
  5. }
  6. }
  1. class App extends PureComponent {
  2. update(e) {
  3. this.props.update(e.target.value);
  4. }
  5. render() {
  6. return <MyInput onChange={this.update.bind(this)}/>;
  7. }
  8. }

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

  1. class App extends PureComponent {
  2. constructor(props) {
  3. super(props);
  4. this.update = this.update.bind(this);
  5. }
  6. update(e) {
  7. this.props.update(e.target.value);
  8. }
  9. render() {
  10. return <MyInput onChange={this.update}/>;
  11. }
  12. }

命名

引用命名

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

  1. // bad
  2. import reservationCard from './ReservationCard';
  3. // good
  4. import ReservationCard from './ReservationCard';
  5. // bad
  6. const ReservationItem = <ReservationCard />;
  7. // good
  8. const reservationItem = <ReservationCard />;

高阶模块命名

  1. // bad
  2. export default function withFoo(WrappedComponent) {
  3. return function WithFoo(props) {
  4. return <WrappedComponent {...props} foo />;
  5. }
  6. }
  7. // good
  8. export default function withFoo(WrappedComponent) {
  9. function WithFoo(props) {
  10. return <WrappedComponent {...props} foo />;
  11. }
  12. const wrappedComponentName = WrappedComponent.displayName
  13. || WrappedComponent.name
  14. || 'Component';
  15. WithFoo.displayName = `withFoo(${wrappedComponentName})`;
  16. return WithFoo;
  17. }

属性命名

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

  1. // bad
  2. <MyComponent style="fancy" />
  3. // good
  4. <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() 方法

如何定义 propTypesdefaultPropscontextTypes, 等等其他属性...

  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. const propTypes = {
  4. id: PropTypes.number.isRequired,
  5. url: PropTypes.string.isRequired,
  6. text: PropTypes.string,
  7. };
  8. const defaultProps = {
  9. text: 'Hello World',
  10. };
  11. class Link extends React.Component {
  12. static methodsAreOk() {
  13. return true;
  14. }
  15. render() {
  16. return <a href={this.props.url} data-id={this.props.id}>{this.props.text}</a>;
  17. }
  18. }
  19. Link.propTypes = propTypes;
  20. Link.defaultProps = defaultProps;
  21. export default Link;
 

React组件设计(转)的更多相关文章

  1. React组件设计

    React组件设计 组件分类 展示组件和容器组件 展示组件 容器组件 关注事物的展示 关注事物如何工作 可能包含展示和容器组件,并且一般会有DOM标签和css样式 可能包含展示和容器组件,并且不会有D ...

  2. React组件设计技巧

    React组件设计 组件分类 展示组件和容器组件 展示组件 容器组件 关注事物的展示 关注事物如何工作 可能包含展示和容器组件,并且一般会有DOM标签和css样式 可能包含展示和容器组件,并且不会有D ...

  3. React 应用设计之道 - curry 化妙用

    使用 React 开发应用,给予了前端工程师无限"组合拼装"快感.但在此基础上,组件如何划分,数据如何流转等应用设计都决定了代码层面的美感和强健性. 同时,在 React 世界里提 ...

  4. 如何优雅的设计 React 组件

    作者:晓冬 本文原创,转载请注明作者及出处 如今的 Web 前端已被 React.Vue 和 Angular 三分天下,一统江山十几年的 jQuery 显然已经很难满足现在的开发模式.那么,为什么大家 ...

  5. 如何优雅的设计React组件

    如何优雅的设计 React 组件 如今的 web 前端已被 React.Vue 和 Angular 三分天下,一统江山十几年的 jQuery 显然已经很难满足现在的开发模式.那么,为什么大家会觉得 j ...

  6. 设计 react 组件

    重新设计 React 组件库 诚身 7 个月前   在 react + redux 已经成为大部分前端项目底层架构的今天, 让我们再次回到软件工程界一个永恒问题的探讨上来, 那就是如何提升一个开发团队 ...

  7. 第二章 设计高质量的React组件

    第二章 设计高质量的React组件 高质量React组件的原则和方法: 划分组件边界的原则: React组件的数据种类: React组件的生命周期. 2.1 易于维护组件的设计要素 1.高内聚:指的是 ...

  8. react 组件构建设计

    项目设计中,可以从顶层React元素开始,然后实现它的子组件,自顶向下来构建组件的层级组件的写法:1.引入依赖模块2.定义React组件3.作为模块导出React组件4.子组件更新父组件的机制5.父组 ...

  9. React 组件间通讯

    React 组件间通讯 说 React 组件间通讯之前,我们先来讨论一下 React 组件究竟有多少种层级间的关系.假设我们开发的项目是一个纯 React 的项目,那我们项目应该有如下类似的关系: 父 ...

随机推荐

  1. Python_问题收录总结

    python IndentationError: unindent does not match any outer indentation level的问题 用python编个作业,我先用的note ...

  2. C# 验证过滤代理IP是否有效

    private void 导入IPToolStripMenuItem_Click(object sender, EventArgs e) { using (OpenFileDialog Openfil ...

  3. HTML5 3D Google搜索 小盒子 大世界

    HTML5真是能让人想象万千,居然动起了Google搜索的主意,它利用HTML5技术将Google搜索放到了一个小盒子里,弄起了3D搜索.随着鼠标移动,HTML5 3D搜索盒子也就转动,非常立体.点击 ...

  4. HTML5 Canvas火焰效果 像火球发射一样

    Canvas是HTML5中非常重要而且有用的东西,我们可以在Canvas上绘制任意的元素,就像你制作Flash一样.今天我们就在Canvas上来制作一款火焰发射的效果.就像古代的火球炮一样,而且可以在 ...

  5. GCT之数学公式(三角函数)

  6. json字符串使用注意问题

    json本身是字符串,即 json字符串 js使用 要把 json字符串 转为  javascript对象 json字符串转为js对象的方法:jquery的parseJSON var str='[{& ...

  7. javascript报错集锦

    1.JS 异常之 missing ) after argument list 错误释疑报错原因:不是字符串就输出啦

  8. 17 HTTP编程入门

    http请求原理 http请求原理我就不多说了,网上一搜就能搜索到,下面我注意是记录下http模块的使用方法 http 模块 HTTP-server hello world 我们使用HandleFun ...

  9. Nginx 反向代理的正确配置

    server { listen 80; server_name 127.0.0.1; #charset koi8-r; #access_log logs/host.access.log main; l ...

  10. 苹果官方xcodeprojectbuild设置指南

    https://developer.apple.com/library/ios/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/ ...