[譯]Thinking in React

編者按

使用React的思想來構建應用對我在實際項目中以及幫助他人解決實際問題時起到了很大做用,因此我翻譯此文來向那些正在或即將陷入React或React-Native深坑的同胞們表示慰問。網上已經有人翻譯過,我想用更易讀的語言翻譯一次,這也是我首次如此一本正經的翻譯技術文章給大衆閱讀,權當練習吧。html

原文地址:https://facebook.github.io/react/docs/thinking-in-react.htmlreact

轉載還請註明出處以及原文地址,出於對做者和譯者勞動成果的尊重吧,謝謝了個人哥。git

Thinking in React

做者:Pete Hunt 譯者:Rex Rao (sohobloo)github

 

我認爲React是使用JavaScript構建高性能大型Web應用的首選方案,咱們已經在Facebook和Instagram中普遍使用,哎喲,效果不錯喲。架構

React的衆多優點之一是——且看它如何讓你能順着思路構建應用。在此,我將引領你用React逐步構建出一個可搜索的商品列表應用。ide

從模型圖開始

假設設計師已經爲咱們提供了API並能夠返回模擬的JSON數據。容我小小鄙視一下這位美工,由於原型圖長成這個挫樣:函數

咱們的API返回的模擬JSON數據長這樣:性能

[
  {category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
  {category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
  {category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
  {category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
  {category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
  {category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];

第一步:拆分並構建界面的組件層次結構樹

你應該作的第一件事是爲你原型圖全部組件和子組件畫個邊框、起個名。要是你跟設計師坐一塊兒,找他們喝喝茶,說不定他們的Photoshop圖層名恰巧能夠用做你React組件的名字!(譯者:我只能說,Too young too simple, sometimes naive!) 單元測試

但你怎麼知道如何拆分一個組件呢?這和你平時決定是否是要新建一個函數或者類的道理同樣同樣的。其中有個叫作單一職責原則的原理,也就是說理想狀態下一個組件只作一件事,當他須要作更多,那就應該繼續拆拆拆。測試

若是你常常向用戶展現JSON數據,你會發現只要你的數據模型建得好,你的界面乃至你的組件架構也會完美的與之映射。由於界面和數據模型傾向於支持相同的信息架構,這讓界面拆分工做變簡單了,拆分出的一個組件只對應展現數據模型中的一種數據就行。

你看,咱這簡單的應用有5種組件。我用斜體標示出了每一個組件要展現的數據。

  1. FilterableProductTable (橙色): 包含整個示例
  2. SearchBar (藍色): 接收用戶輸入(user input)
  3. ProductTable (綠色): 顯示基於用戶輸入(user input)過濾的數據集 (data collection)
  4. ProductCategoryRow (青色): 顯示分類( category)
  5. ProductRow (紅色): 顯示每一行商品(product)

ProductTable你會發現表頭(含"Name"和"Price"標籤)並無拆分紅組件,這是出於一種存在爭議的我的喜愛而已啦。這個例子中,既然渲染數據集 (data collection)ProductTable的職責,那就讓它做爲此組件的一部分好了。要是它再複雜一點的話(好比排序功能),那就另當別論獨立成ProductTableHeader組件咯。

讓咱們把從原型圖中定義的組件組合成層次結構樹。若是一個組件出如今另外一個組件中,那麼這個組件就是它的子組件,so easy:

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

第二步:用React作個靜態版

var ProductCategoryRow = React.createClass({
  render: function() {
    return (<tr><th colSpan="2">{this.props.category}</th></tr>);
  }
});

var ProductRow = React.createClass({
  render: function() {
    var name = this.props.product.stocked ?
      this.props.product.name :
      <span style={{color: 'red'}}>
        {this.props.product.name}
      </span>;
    return (
      <tr>
        <td>{name}</td>
        <td>{this.props.product.price}</td>
      </tr>
    );
  }
});

var ProductTable = React.createClass({
  render: function() {
    var rows = [];
    var lastCategory = null;
    this.props.products.forEach(function(product) {
      if (product.category !== lastCategory) {
        rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
      }
      rows.push(<ProductRow product={product} key={product.name} />);
      lastCategory = product.category;
    });
    return (
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Price</th>
          </tr>
        </thead>
        <tbody>{rows}</tbody>
      </table>
    );
  }
});

var SearchBar = React.createClass({
  render: function() {
    return (
      <form>
        <input type="text" placeholder="Search..." />
        <p>
          <input type="checkbox" />
          {' '}
          Only show products in stock
        </p>
      </form>
    );
  }
});

var FilterableProductTable = React.createClass({
  render: function() {
    return (
      <div>
        <SearchBar />
        <ProductTable products={this.props.products} />
      </div>
    );
  }
});


var PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];
 
ReactDOM.render(
  <FilterableProductTable products={PRODUCTS} />,
  document.getElementById('container')
);
View Code

有了組件層次結構,是時候表演真正的技術了實現你的應用了。最簡單的方式是把數據渲染到界面上,可是不帶交互功能。最好是分離這些步驟,由於構建一個靜態版本更可能是須要你敲鍵盤而增長交互功能就須要你敲腦殼了。

將你的應用構建出一個靜態版原本展現數據模型,你也許會須要構建組件來複用其餘組件,用屬性(props)傳入數據。 屬性(props)是一種將數據從父組件傳入子組件的途徑。 即使你對狀態(state)模式很是熟悉,在靜態版本中也不要使用狀態哦。狀態是留給交互來處理那些會變化的數據使用的。做爲一個靜態版請無視之。

你能夠用自上而下或自下而上的方式構建應用。既能夠從最頂層組件開始(好比從FilterableProductTable開始)也能夠從最底層組件開始(如ProductRow)。在簡單的示例中,自上而下每每更容易;而在大型項目中,使用自下而上更好,你還能方便的寫單元測試呢!

這一步完成以後,你就有了一個能夠展現你的數據模型的組件庫。做爲一個靜態版本,每一個組件都只有一個 render()方法。頂層組件(FilterableProductTable)經過屬性(prop)得到你的數據模型。若是此時你改變你的基礎數據模型並再次調用ReactDOM.render(),界面會刷新。界面的刷新和變化了一目瞭然直接了當。React的單向數據流(又名單向綁定)讓全部事情有序且快速。

若是在這一步中遇到問題,你能夠參考React文檔

 

小插曲兒: 屬性(props)與狀態(state)

React中有兩類數據模型:屬性和狀態,瞭解他們的區別是頗有必要的!還不太清楚?來來來看這裏React官方文檔咋說的。

第三步:定義最小(完整)的界面狀態值

界面想要動起來?數據必須變起來!React使用狀態(state)來實現。

若想正確構建你的應用,首先你得考慮你的應用至少須要一組什麼樣的可變狀態值。來跟我念口訣:取其精華,去其糟粕。找出你的應用的那組乾貨——絕對最小化的界面狀態值組,而且其餘任何須要均可以通這組值計算得出。好比你要構建一個待辦列表,只須要維護一組待辦項便可;你不須要再維護這組列表的個數的值,由於在你須要展現待辦數時能夠直接獲取列表長度獲得結果。

來看看咱們例子裏有哪些數據:

  • 原始的商品列表
  • 用戶輸入的搜索文本
  • 勾選框的值
  • 篩選後的商品列表

讓咱們逐條看看哪些是狀態,對每一條數據三省吾身:

  1. 是不是父組件傳入的屬性?若是是的,估計不是狀態。
  2. 是否會隨時間變化改變?若是不會變,估計不是狀態。
  3. 可否從其餘狀態或屬性計算獲得?若是能夠,確定不是狀態。

原始商品列表經過屬性傳進來,所以它不是狀態。搜索框的值和勾選框的值能夠改變並且其餘東西也計算不出來這些值,看上去應該是狀態。最後,篩選後的商品列表也不是狀態,由於它能夠經過原始商品列表、搜索框的值和勾選框的值計算得出。

最後得出咱們須要的狀態:

  • 用戶輸入的輸入框的值
  • 勾選框的值

第四步:給狀態找個家

最小狀態集新鮮出爐,接下來咱們須要定義哪些組件會變化,或者說擁有這些狀態。

記住了: React數據老是單向且「下流」的——流向組件層次中的底層。可能並非一開始就看得出哪一個組件擁有什麼狀態。這經常是萌新最難理解的部分,因此就讓老司機帶帶你吧:

對於你應用的每一條狀態:

  • 找出每個須要基於此狀態來渲染界面的組件。
  • 找到它們共同的爹(一個在組件層次中須要此狀態的全部控件的頂層父組件)。
  • 它們共同的父組件或更高層級的組件均可以做爲狀態的持有者。
  • 若是你以爲哪一個組件持有這個狀態都很彆扭,能夠爲了這個狀態創造一個新的組件來持有,並把這個新組件加到它們共同父組件的上層結構中的任何合適位置。

針對咱們的應用,讓咱們根據以上策略捋一捋:

  • ProductTable須要根據狀態值來過濾商品列表,SearchBar須要顯示搜索文本和勾選框狀態值。
  • FilterableProductTable是它們的共同父組件。
  • 看起來搜索文本和勾選框值放在FilterableProductTable挺合適。

就這麼愉快的決定了,把這些狀態放FilterableProductTable裏吧。首先在FilterableProductTable中增長getInitialState()(譯者:ES6中若是用class構建組件,初始化狀態的方法將發生改變方法並返回{filterText: '', inStockOnly: false}來對應應用的初始狀態。而後將filterText和inStockOnly做爲屬性傳給ProductTableSearchBar。最後就用屬性來過濾ProductTable中的商品列表並把搜索文本設置到SearchBar的輸入框中。

來看看你應用的表現如何:把filterText設置成"ball"而後刷新。厲害了個人哥,列表正確的更新了!

第五步:增長反向數據流

至此,咱們已經構建了一個能正確渲染屬性和狀態從組件層次自上而下傳遞的應用了。是時候表演真正的技術了支持數據反向傳遞了:底層組件須要更新FilterableProductTable裏的狀態。

React明確的數據傳遞能讓你更容易搞清楚你的程序是怎麼運做的,但比起傳統的雙向數據綁定你就須要敲稍微多一點的代碼了。 React提供了一個叫ReactLink的插件來讓這種模式變得和雙向綁定同樣方便,但本文的目的在於讓一切明晰,暫不使用。

若是你嘗試在當前版本的示例中輸入或勾選,你會發現React徹底無視你的輸入。 怎麼回事難道有Bug?乖乖咱們故意的!由於咱們剛纔把input的value屬性設置成老是等於FilterableProductTable傳進來的狀態了。

然並卵,咱們須要用戶的輸入馬上更新狀態。既然控件只容許更新本身的狀態,FilterableProductTable能夠傳一個每次狀態須要發生變化時都會觸發的回調函數回傳到SearchBar。咱們能夠用輸入框的onChange事件來觸發並在FilterableProductTable傳入的回調函數中調用setState()來更新狀態。

看上去好像很複雜的樣子,其實只是多了幾行代碼而已,但這真真真的讓你能看清數據是如何在你應用的身體裏流來流去的。

沒錯就是這樣

但願這篇文章能在你用React構建組件或應用時給你點亮一盞明燈。雖然可能比之前要搬更多磚,但請你記住代碼寫出來是要能夠給人閱讀的,特別是那些標準統1、邏輯清晰的代碼更賞心悅目。當你開始構建大型的控件庫的時候,你會感激這種規則化、清晰化的風格,再加上代碼的複用,你的代碼行數會獲得縮減。☺

相關文章
相關標籤/搜索