React編程思想

本文是對React官網《Thinking in React》一文的翻譯,經過這篇文章,React團隊向開發者們介紹了應該若是去構思一個web應用,爲從此使用React進行web app的構建,打下基礎。 如下是正文。css

在咱們團隊看來,React是使用JavaScript構建大型、快速的Web apps的首選方式。它已經在Facebook和Instagram項目中,表現出了很是好的可擴展性。html

可以按照構建的方式來思考web app的實現,是React衆多優勢之一。在這篇文章中,咱們將引導你進行使用React構建可搜索產品數據表的思考過程。react

從設計稿開始

想象一下,咱們已經有了一個JSON API和來自設計師的設計稿。以下圖所示:web

Mockup

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"}
];
複製代碼

第一步:將UI分解爲組件並分析層級結構

咱們要作的第一件事就是給設計稿中的每一個組件(和子組件)畫框,並給它們起名字。若是你正在和一個設計師合做,他可能已經幫你完成了這一步。他的Photoshop圖層名稱可能最終會成爲你的React組件名稱!數組

但咱們怎麼知道本身的組件應該是什麼?只須要使用一些通用的技巧來決定是否應該建立一個新的函數或對象。其中一個技巧叫作:單一責任原則。就是說,在理想狀況下,一個組件應該只用來完成一件事。若非如此,則應該考慮將其分解成更小的子組件。架構

咱們常常會向用戶展現JSON數據模型,那麼你應該會發現,若是模型構建正確,那麼你的UI(以及組件結構)應該可以很好地映射數據模型。這是由於UI和數據模型傾向於遵循相同的信息架構,這意味着將UI分解爲組件的工做一般是微不足道的。如今咱們把它分解成映射數據模型的組件以下:app

Component diagram

如今咱們的示例應用中有了五個組件,並且咱們將每一個組件表明的數據用斜體表示以下:模塊化

  1. **FilterableProductTable **(橘黃色):包含整個示例的組件
  2. **SearchBar **(藍色):接收全部的用戶輸入
  3. **ProductTable **(綠色):根據用戶輸入顯示和過濾數據集
  4. **ProductCategoryRow **(綠寶石色):顯示分類頭
  5. **ProductRow **(紅色):每行顯示一條商品數據

細心的你會發現,在ProductTable中,表頭(包含名稱價格標籤)不是一個組件。這是一個偏好的問題,有兩個方面的論點。在這個例子中,咱們將其做爲ProductTable組件的一部分,由於它是ProductTable負責渲染的數據集的一部分。可是,若是這個頭部變得很複雜(好比咱們要支持排序),那麼將其設置爲ProductTableHeader這樣的組件確定會更好一些。函數

如今咱們已經肯定了設計稿中的組件,下一步咱們要給這些組件安排層次結構。這其實很容易:出如今一個組件中的組件應該在層次結構中顯示爲一個子組件:

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

第二步:用React構建一個靜態版本

class ProductCategoryRow extends React.Component {
  render() {
    const category = this.props.category;
    return (
      <tr>
        <th colSpan="2">
          {category}
        </th>
      </tr>
    );
  }
}

class ProductRow extends React.Component {
  render() {
    const product = this.props.product;
    const name = product.stocked ?
      product.name :
      <span style={{color: 'red'}}>
        {product.name}
      </span>;

    return (
      <tr>
        <td>{name}</td>
        <td>{product.price}</td>
      </tr>
    );
  }
}

class ProductTable extends React.Component {
  render() {
    const rows = [];
    let lastCategory = null;
    
    this.props.products.forEach((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>
    );
  }
}

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

class FilterableProductTable extends React.Component {
  render() {
    return (
      <div>
        <SearchBar />
        <ProductTable products={this.props.products} />
      </div>
    );
  }
}


const 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')
);
複製代碼

如今咱們已經有了組件層次結構,接下來能夠實現應用程序了。最初的方案是構建一個使用數據模型渲染UI但不具備交互性的版本。最好將靜態版本和添加交互性進行解耦,由於構建一個靜態的版本須要大量的輸入卻不須要思考,而增長交互性須要大量的思考而不須要不少輸入。咱們一下子會知道爲何。

要構建渲染數據模型的靜態版本,須要構建可複用其餘組件並使用props傳遞數據的組件。props是一種將數據從父組件傳遞給子組件的方式。若是你熟悉state的概念,請不要使用state來構建這個靜態版本。state只爲實現交互性而保留,即隨時間變化的數據。因爲這是應用程序的靜態版本,因此暫時不須要它。

你的構建過程能夠自上而下或自下而上。也就是說,你能夠從構建層次較高的組件(即FilterableProductTable)開始或較低的組件(ProductRow開始)。在簡單的例子中,自上而下一般比較容易,而在大型項目中,自下而上更容易並且更易於編寫測試用例

在這一步的最後,你會有一個可重用組件的庫來渲染你的數據模型。這些組件只會有render()方法,由於這是你的應用程序的靜態版本。層次結構頂部的組件(FilterableProductTable)將把你的數據模型做爲一個prop。若是你對基礎數據模型進行更改並再次調用ReactDOM.render(),則UI將會更新。這就很容易看到用戶界面是如何更新以及在哪裏進行更改了,由於沒有任何複雜的事情發生。 React的單向數據流(也稱爲單向綁定)使全部的事務更加模塊化也更加快速。

第三步:肯定UI狀態的最小(但完整)表示形式

爲了使你的UI具備交互性,須要可以觸發對基礎數據模型的更改。 React使用state讓這一切變得簡單。要正確構建應用程序,首先須要考慮應用程序須要的最小可變狀態集。這裏的關鍵是:不要重複本身。找出應用程序須要的狀態的絕對最小表示,並計算須要的其餘全部內容。例如,若是你正在建立一個TODO列表,只須要保存一個TODO項目的數組;不要爲計數保留一個單獨的狀態變量。相反,當你要渲染TODO數量時,只需取TODO項目數組的長度便可。

考慮咱們示例應用程序中的全部數據。咱們有:

  • 產品的原始列表
  • 用戶輸入的搜索文本
  • 複選框的值
  • 過濾的產品列表

咱們來看看每個是哪個state。這裏有關於每條數據的三個問題:

  1. 是經過props從父組件傳入的嗎?若是是,那可能不是state。
  2. 它是否保持不變?若是是,那可能不是state。
  3. 你能基於組件中的任何其餘state或props來計算它嗎?若是是,那不是state。

原來的產品清單是做爲props傳入的,因此這不是state。搜索文本和複選框彷佛是state,由於它們隨着時間而改變,不能從任何東西計算。最後,產品的過濾列表不是state,由於它能夠經過將產品的原始列表與複選框的搜索文本和值組合來計算獲得。

因此最後,咱們的states是:

  • 用戶輸入的搜索文本
  • 複選框的值

第四步: 肯定你的state須要放置在什麼地方

class ProductCategoryRow extends React.Component {
  render() {
    const category = this.props.category;
    return (
      <tr>
        <th colSpan="2">
          {category}
        </th>
      </tr>
    );
  }
}

class ProductRow extends React.Component {
  render() {
    const product = this.props.product;
    const name = product.stocked ?
      product.name :
      <span style={{color: 'red'}}>
        {product.name}
      </span>;

    return (
      <tr>
        <td>{name}</td>
        <td>{product.price}</td>
      </tr>
    );
  }
}

class ProductTable extends React.Component {
  render() {
    const filterText = this.props.filterText;
    const inStockOnly = this.props.inStockOnly;

    const rows = [];
    let lastCategory = null;

    this.props.products.forEach((product) => {
      if (product.name.indexOf(filterText) === -1) {
        return;
      }
      if (inStockOnly && !product.stocked) {
        return;
      }
      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>
    );
  }
}

class SearchBar extends React.Component {
  render() {
    const filterText = this.props.filterText;
    const inStockOnly = this.props.inStockOnly;

    return (
      <form>
        <input
          type="text"
          placeholder="Search..."
          value={filterText} />
        <p>
          <input
            type="checkbox"
            checked={inStockOnly} />
          {' '}
          Only show products in stock
        </p>
      </form>
    );
  }
}

class FilterableProductTable extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      filterText: '',
      inStockOnly: false
    };
  }

  render() {
    return (
      <div>
        <SearchBar
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
        />
        <ProductTable
          products={this.props.products}
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
        />
      </div>
    );
  }
}


const 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')
);
複製代碼

如今咱們已經肯定了最小的一組應用程序state。接下來,咱們須要肯定哪一個組件會改變或擁有這個state。

請記住:數據在React的組件層次結構中是單向流動的。它可能不清楚哪一個組件應該擁有什麼狀態。這一般是新手理解的最具挑戰性的部分,因此請按照如下步驟解決:

對於你的應用程序中的每個state:

  • 肯定基於該state渲染某些內容的每一個組件。
  • 找到一個共同的擁有者組件(一個在全部須要該state的層次結構組件之上的組件)。
  • 不管是共同全部者,仍是高層次的其餘組成部分,都應該擁有這個state。
  • 若是你沒法找到一個有意義的組件,那麼只好建立一個新的組件來保存state,並將其添加到公共全部者組件上方的層次結構中的某個位置。

讓咱們來看看咱們的應用程序的這個策略:

  • ProductTable須要根據狀態過濾產品列表,而SearchBar須要顯示搜索文本和檢查狀態。
  • 通用全部者組件是FilterableProductTable
  • 從概念上講,過濾器文本和選中的值存在於FilterableProductTable中是有意義的

酷,因此咱們已經決定,咱們的state存活在FilterableProductTable中。首先,將一個實例屬性this.state = {filterText:'',inStockOnly:false}添加到FilterableProductTable的構造函數中,以反映應用程序的初始狀態。而後,將filterTextinStockOnly做爲prop傳遞給ProductTableSearchBar。最後,使用這些props來篩選ProductTable中的行,並在SearchBar中設置表單域的值。

你能夠看到你的應用程序的行爲了:設置filterText爲「ball」,並刷新你的應用程序。你將看到數據表已正確更新。

第五步:添加反向數據流

class ProductCategoryRow extends React.Component {
  render() {
    const category = this.props.category;
    return (
      <tr>
        <th colSpan="2">
          {category}
        </th>
      </tr>
    );
  }
}

class ProductRow extends React.Component {
  render() {
    const product = this.props.product;
    const name = product.stocked ?
      product.name :
      <span style={{color: 'red'}}>
        {product.name}
      </span>;

    return (
      <tr>
        <td>{name}</td>
        <td>{product.price}</td>
      </tr>
    );
  }
}

class ProductTable extends React.Component {
  render() {
    const filterText = this.props.filterText;
    const inStockOnly = this.props.inStockOnly;

    const rows = [];
    let lastCategory = null;

    this.props.products.forEach((product) => {
      if (product.name.indexOf(filterText) === -1) {
        return;
      }
      if (inStockOnly && !product.stocked) {
        return;
      }
      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>
    );
  }
}

class SearchBar extends React.Component {
  constructor(props) {
    super(props);
    this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
    this.handleInStockChange = this.handleInStockChange.bind(this);
  }
  
  handleFilterTextChange(e) {
    this.props.onFilterTextChange(e.target.value);
  }
  
  handleInStockChange(e) {
    this.props.onInStockChange(e.target.checked);
  }
  
  render() {
    return (
      <form>
        <input
          type="text"
          placeholder="Search..."
          value={this.props.filterText}
          onChange={this.handleFilterTextChange}
        />
        <p>
          <input
            type="checkbox"
            checked={this.props.inStockOnly}
            onChange={this.handleInStockChange}
          />
          {' '}
          Only show products in stock
        </p>
      </form>
    );
  }
}

class FilterableProductTable extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      filterText: '',
      inStockOnly: false
    };
    
    this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
    this.handleInStockChange = this.handleInStockChange.bind(this);
  }

  handleFilterTextChange(filterText) {
    this.setState({
      filterText: filterText
    });
  }
  
  handleInStockChange(inStockOnly) {
    this.setState({
      inStockOnly: inStockOnly
    })
  }

  render() {
    return (
      <div>
        <SearchBar
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
          onFilterTextChange={this.handleFilterTextChange}
          onInStockChange={this.handleInStockChange}
        />
        <ProductTable
          products={this.props.products}
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
        />
      </div>
    );
  }
}


const 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')
);
複製代碼

到目前爲止,咱們已經構建了一個應用程序,能夠根據props和state正確地呈如今層次結構中。如今是時候以另外一種方式支持數據流:深層次的表單組件須要更新FilterableProductTable中的狀態。

React使這個數據流清晰易懂,以便理解你的程序是如何工做的,可是它須要比傳統的雙向數據綁定更多的輸入。

若是你嘗試在當前版本的示例中鍵入或選中該框,則會看到React忽略了你的輸入。這是故意的,由於咱們已經將輸入的值prop設置爲始終等於從FilterableProductTable傳入的state。

讓咱們想一想咱們想要發生的事情。咱們但願確保每當用戶更改表單時,咱們都會更新狀態以反映用戶的輸入。因爲組件應該只更新本身的state,只要state須要更新時,FilterableProductTable就會傳遞迴調到SearchBar。咱們可使用輸入上的onChange事件來通知它。 FilterableProductTable傳遞的回調將調用setState(),而且應用程序將被更新。

雖然這聽起來很複雜,但實際上只是幾行代碼。你的數據如何在整個應用程序中流動變得很是明確。

就是這樣

但願這篇文章可讓你瞭解如何用React來構建組件和應用程序。雖然它可能比之前多一些代碼,但請記住,代碼的讀遠遠超過它的寫,而且讀取這個模塊化的顯式代碼很是容易。當你開始構建大型組件庫時,你將會體會到這種明確性和模塊性,而且經過代碼重用,你的代碼行將開始縮小。

備註

文中全部示例的HTML和CSS內容以下:

<div id="container">
    <!-- This element's contents will be replaced with your component. -->
</div>
複製代碼
body {
  padding: 5px
}
複製代碼
相關文章
相關標籤/搜索