React 最佳實踐——那些 React 沒告訴你但很重要的事

前言:對不少 react 新手來講,網上能找到的資源大都是些簡單的 tutorial ,它們能教會你如何使用 react ,但並不會告訴你怎麼在實際項目中優雅的組織和編寫 react 代碼。用谷歌搜中文「 React 最佳實踐」發現前兩頁幾乎全都是同一篇國外文章的譯文...因此我總結了下本身過去那個項目使用 React 踩過的一些坑,也整理了一些別人的觀點,但願對部分 react 使用者有幫助。javascript

React 與 AJAX

React只負責處理View這一層,它自己不涉及網絡請求/AJAX,因此這裏咱們需求考慮兩個問題:html

  • 第一,用什麼技術從服務端獲取數據;java

  • 第二,獲取到的數據應該放在react組件的什麼位置。react

React官方提供了一種解決方案:Load Initial Data via AJAXios

使用jQuery的Ajax方法,在一個組件的componentDidMount()中發ajax請求,拿到的數據存在組件本身的state中,並調用setState方法去更新UI。若是是異步獲取數據,則在componentWillUnmount中取消發送請求。git

若是隻是爲了使用jQuery的Ajax方法就引入整個jQuery庫,既是一種浪費又加大了整個應用的體積。那咱們還有什麼其餘的選擇嗎?事實上是有不少的:fetch()fetch polyfillaxios...其中最須要咱們關注的是window.fetch(),它是一個簡潔、標準化的javascript的Ajax API。在Chrome和Firefox中已經可使用,若是須要兼容其餘瀏覽器,可使用fetch polyfill。github

React官方文檔只告訴了咱們在一個單一組件中如何經過ajax從服務器端獲取數據,但並無告訴咱們在一個完整的實際項目中到底應該把數據存在哪些組件中,這部分若是缺少規範的話,會致使整個項目變得混亂、難以維護。下面給出三種比較好的實踐:ajax

1. 全部的數據請求和管理都存放在惟一的一個根組件

讓父組件/根組件集中發送全部的ajax請求,把從服務端獲取的數據統一存放在這個組件的state中,再經過props把數據傳給子組件。這種方法主要是針對組件樹不是很複雜的小型應用。缺點就是當組件樹的層級變多了之後,須要把數據一層一層地傳給子組件,寫起來麻煩,性能也很差。express

2. 設置多個容器組件專門處理數據請求和管理

其實跟第一種方法相似,只不過設置多個容器組件來負責數據請求和狀態管理。這裏咱們須要區分兩種不一樣類型的組件,一種是展現性組件(presentational component),另外一種是容器性組件(container component)。展現性組件自己不擁有任何狀態,全部的數據都從容器組件中得到,在容器組件中發送ajax請求。二者更詳細的描述,能夠閱讀下這篇文章:Presentational and Container Componentsredux

一個具體的例子:

假設咱們須要展現用戶的姓名和頭像,首先建立一個展現性組件<UserProfile />,它接受兩個Props:nameprofileImage。這個組件內部沒有任何關於Ajax的代碼。

而後建立一個容器組件<UserProfileContainer />,它接受一個userId的參數,發送Ajax請求從服務器獲取數據存在state中,再經過props傳給<UserProfile />組件。

3. 使用Redux或Relay的狀況

Redux管理狀態和數據,Ajax從服務器端獲取數據,因此很顯然當咱們使用了Redux時,應該把全部的網絡請求都交給redux來解決。具體來講,應該是放在Async Actions。若是用其餘類Flux庫的話,解決方式都差很少,都是在actions中發送網絡請求。

Relay是Facebook官方推出的一個庫。若是用它的話,咱們只須要經過GraphQL來聲明組件須要的數據,Relay會自動地把下載數據並經過props往下傳遞。不過想要用Relay,你得先有一個GraphQL的服務器...

一個標準組件的組織結構

1 class definition
    1.1 constructor
        1.1.1 event handlers
    1.2 'component' lifecycle events
    1.3 getters
    1.4 render
2 defaultProps
3 proptypes

示例:

class Person extends React.Component {
  constructor (props) {
    super(props);

    this.state = { smiling: false };

    this.handleClick = () => {
      this.setState({smiling: !this.state.smiling});
    };
  }

  componentWillMount () {
    // add event listeners (Flux Store, WebSocket, document, etc.)
  },

  componentDidMount () {
    // React.getDOMNode()
  },

  componentWillUnmount () {
    // remove event listeners (Flux Store, WebSocket, document, etc.)
  },

  get smilingMessage () {
    return (this.state.smiling) ? "is smiling" : "";
  }

  render () {
    return (
      <div onClick={this.handleClick}>
        {this.props.name} {this.smilingMessage}
      </div>
    );
  },
}

Person.defaultProps = {
  name: 'Guest'
};

Person.propTypes = {
  name: React.PropTypes.string
};

以上示例代碼的來源:https://github.com/planningcenter/react-patterns#component-organization

使用 PropTypes 和 getDefaultProps()

  1. 必定要寫PropTypes,切莫爲了省事而不寫

  2. 若是一個Props不是requied,必定在getDefaultProps中設置它

    React.PropTypes主要用來驗證組件接收到的props是否爲正確的數據類型,若是不正確,console中就會出現對應的warning。出於性能方面的考慮,這個API只在開發環境下使用。

基本使用方法:

propTypes: {
    myArray: React.PropTypes.array,
    myBool: React.PropTypes.bool,
    myFunc: React.PropTypes.func,
    myNumber: React.PropTypes.number,
    myString: React.PropTypes.string,
     
     // You can chain any of the above with `isRequired` to make sure a warning
    // is shown if the prop isn't provided.
    requiredFunc: React.PropTypes.func.isRequired
}

假如咱們props不是以上類型,而是擁有複雜結構的對象怎麼辦?好比下面這個:

{
  text: 'hello world',
  numbers: [5, 2, 7, 9],
}

固然,咱們能夠直接用React.PropTypes.object,可是對象內部的數據咱們卻沒法驗證。

propTypes: {
  myObject: React.PropTypes.object,
}

進階使用方法:shape()arrayOf()

propTypes: {
  myObject: React.PropTypes.shape({
    text: React.PropTypes.string,
    numbers: React.PropTypes.arrayOf(React.PropTypes.number),
  })
}

下面是一個更復雜的Props:

[
  {
    name: 'Zachary He',
    age: 13,
    married: true,
  },
  {
    name: 'Alice Yo',
    name: 17,
  },
  {
    name: 'Jonyu Me',
    age: 20,
    married: false,
  }
]

綜合上面,寫起來應該就不難了:

propTypes: {
    myArray: React.PropTypes.arrayOf(
        React.propTypes.shape({
            name: React.propTypes.string.isRequired,
            age: React.propTypes.number.isRequired,
            married: React.propTypes.bool
        })
    )
}

把計算和條件判斷都交給 render() 方法吧

1. 組件的state中不能出現props
// BAD:
  constructor (props) {
    this.state = {
      fullName: `${props.firstName} ${props.lastName}`
    };
  }

  render () {
    var fullName = this.state.fullName;
    return (
      <div>
        <h2>{fullName}</h2>
      </div>
    );
  }
// GOOD: 
render () {
  var fullName = `${this.props.firstName} ${this.props.lastName}`;
}

固然,複雜的display logic也應該避免全堆放在render()中,由於那樣可能致使整個render()方法變得臃腫,不優雅。咱們能夠把一些複雜的邏輯經過helper function移出去。

// GOOD: helper function
renderFullName () {
  return `${this.props.firstName} ${this.props.lastName}`;
}

render () {
  var fullName = this.renderFullName();
}
2. 保持state的簡潔,不要出現計算得來的state
// WRONG:
  constructor (props) {
    this.state = {
      listItems: [1, 2, 3, 4, 5, 6],
      itemsNum: this.state.listItems.length
    };
  }
  render() {
      return (
          <div>
              <span>{this.state.itemsNum}</span>
          </div>
      )
  }
// Right:
render () {
  var itemsNum = this.state.listItems.length;
}
3. 能用三元判斷符,就不用If,直接放在render()裏
// BAD: 
renderSmilingStatement () {
    if (this.state.isSmiling) {
        return <span>is smiling</span>;
    }else {
        return '';
    }
},

render () {
  return <div>{this.props.name}{this.renderSmilingStatement()}</div>;
}
// GOOD: 
render () {
  return (
    <div>
      {this.props.name}
      {(this.state.smiling)
        ? <span>is smiling</span>
        : null
      }
    </div>
  );
}
4. 布爾值都不能搞定的,交給IIFE吧

Immediately-invoked function expression

return (
  <section>
    <h1>Color</h1>
    <h3>Name</h3>
    <p>{this.state.color || "white"}</p>
    <h3>Hex</h3>
    <p>
      {(() => {
        switch (this.state.color) {
          case "red":   return "#FF0000";
          case "green": return "#00FF00";
          case "blue":  return "#0000FF";
          default:      return "#FFFFFF";
        }
      })()}
    </p>
  </section>
);
5. 不要把display logic寫在componentWillReceivePropscomponentWillMount中,把它們都移到render()中去。

如何動態處理 classNames

1. 使用布爾值
// BAD:
constructor () {
    this.state = {
      classes: []
    };
  }

  handleClick () {
    var classes = this.state.classes;
    var index = classes.indexOf('active');

    if (index != -1) {
      classes.splice(index, 1);
    } else {
      classes.push('active');
    }

    this.setState({ classes: classes });
  }
// GOOD:
  constructor () {
    this.state = {
      isActive: false
    };
  }

  handleClick () {
    this.setState({ isActive: !this.state.isActive });
  }
2. 使用classnames這個小工具來拼接classNames:
// BEFORE:
var Button = React.createClass({
  render () {
    var btnClass = 'btn';
    if (this.state.isPressed) btnClass += ' btn-pressed';
    else if (this.state.isHovered) btnClass += ' btn-over';
    return <button className={btnClass}>{this.props.label}</button>;
  }
});
// AFTER:
var classNames = require('classnames');

var Button = React.createClass({
  render () {
    var btnClass = classNames({
      'btn': true,
      'btn-pressed': this.state.isPressed,
      'btn-over': !this.state.isPressed && this.state.isHovered
    });
    return <button className={btnClass}>{this.props.label}</button>;
  }
});

未完待續...

相關文章
相關標籤/搜索