[譯]React核心概念11:以React的方式思考

原文連接:reactjs.org/docs/thinki…html

引言

在咱們看來React是構建大型快速反應的Web應用的首選方式。咱們已經在Facebook和Instagram裏面證明了React可以運行地很是完美。react

React最棒的部分之一是引導咱們思考如何構建一個應用。在本章中,咱們將會帶領你領略使用React構建產品搜索應用的全過程。數組

從設計稿開始

想象咱們已經有了JSON的API和設計手稿。設計手稿以下:bash

咱們的JSON API返回的數據格式以下:

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

步驟1:根據UI劃分組件層次

第一件須要作的事是在設計稿上畫出組件(包括子組件)並給他們命名。若是你有一個設計師,那麼他們可能早就已經作好了這步工做,只要去問他們就能夠了。可能PS上的圖層名就是最終的組件名哦。模塊化

可是咱們怎麼知道什麼纔是組件呢?就像你決定是否建立一個函數或者對象同樣,根據單一功能原則來決定。組件應該只作一件事,若是組件包含了許多功能那麼就須要將它再次分解成更小的組件。函數

因爲咱們常常向用戶展現JSON數據模型,若是你的模型構建的準確,你都UI(或者說組件結構)就會與數據模型一一對應。這是由於UI與數據模型傾向於遵循相同的信息結構。將UI分紅組件,其中組件須要與數據模型中的某一部分向匹配。post

根據上圖咱們能夠看到在用用中咱們總共有5個組件。這裏咱們用斜體字表示每一個組件所表明的含義:

  1. FilterableProductTable(橙色):做爲整個應用的容器組件;
  2. SearchBar(藍色):接收用戶輸入
  3. ProductTable(綠色):根據用戶輸入展現對應的數據集合
  4. ProductCategoryRow(藍綠色):展現分類的標題
  5. ProductRow(紅色):展現一行產品

如今咱們看向ProductTable,咱們會發現表頭(包含「Name」和「Price」)不是一個組件,但這能夠根據我的喜愛來決定是否要把它提成一個組件。在本例中,咱們把它做爲ProductTable的一部分,由於渲染數據集合本就是ProductTable的工做。可是若是表頭是複雜的(好比包含有排序功能),那麼將他提取成ProductTableHeader就變得有必要了。測試

如今咱們就定義好了咱們的組件,讓咱們把它們按照層級排好。在設計稿中出如今其餘組件內的組件在層級上應該做爲該組件的子組件顯式:ui

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

步驟2:用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。由於建立靜態版本須要編寫許多代碼並且過程缺乏思考,因此在這個過程當中咱們最好將他們解耦。而添加交互的過程不須要過多的編碼,須要的是思考。接下來咱們就來看看爲何須要這樣作。this

爲了建立一個可以渲染數據模型的靜態版本的應用,咱們須要建立可以複用其餘組件而且將數據做爲props傳遞下去的組件。props是將數據從父組件傳遞給子組件的一種數據傳遞方式。若是你熟悉state的概念,請不要在構建靜態版本時使用state,由於state存儲的數據是變化的而這正適合交互時數據變化的特性,因此state須要爲交互設計預留。因此在構建靜態版本時咱們不須要使用state。

你能夠自上而下或者自下而上地構建應用,換而言之,就是你能夠從構建組件層級中最高層開始(如FilterableProductTable)或者從層級中較低層開始(如ProductRow)。在簡單的應用中,一般來講自上而下構建應用更加簡單,但在大型應用中,自下而上是更好的方法,你能夠直接測試你編寫的部分。

在這步的最後,咱們將會擁有用來構建應用的組件庫。由於如今只是構建靜態版本因此每一個組件都只有render()方法。最高層級組件FilterableProductTable會將數據模型做爲prop傳遞給其餘組件。若是你修改了數據而且再次調用ReactDOM.render()方法,那麼UI將會被更新,你能夠看到UI是如何根據數據來變化的。React的單向數據流(也成爲單向綁定)使得組件模塊化更加易於開發。

補充說明:props和state

理解React中的兩種數據模型props和state是很是重要的。若是對於這二者的區別還不是很瞭解,能夠查看state&生命週期,也能夠查看FAQ:props和state的區別

步驟3:肯定UI狀態的最小完備集

爲了是你的UI具備交互能力,就須要具備對基本數據進行修改的能力。React經過state達到這個目的。

爲了正確地構建你的應用,首先咱們須要肯定應用須要的最小可修改state集合。肯定集合的關鍵是:不要重複數據。確保你的應用能夠經過這個數據集計算出其餘全部須要的數據。好比,你如今證實構建一個待辦事項清單,只須要保存待辦事項的數組就能夠了,不須要保存待辦事項的數量,若是你想要知道待辦事項的數目,只須要經過數組的length就能夠知道了。

如今咱們來看看應用中須要的數據,咱們有:

  • 產品列表
  • 用戶輸入的搜索文本
  • 單選框的值
  • 篩選出來的產品列表

如今讓咱們看看哪個數據能夠做爲state保存。在判斷的時候問本身如下三個問題:

  1. 這是父組件經過props傳遞進來的嗎?若是是,那它不是一個state;
  2. 它是否隨時間推移保持不變?若是是,那它應該不是一個state;
  3. 你能根據其餘state或者props推導出這個數據嗎?若是是,那它確定不是state;

產品列表是經過props傳遞的,因此它不是一個state。用戶輸入的搜索文本和單選框的值是能夠隨着時間改變的而且不髮根據其餘值計算出來,因此它們是state。最後,篩選後的產品列表不是state,由於它能夠經過原始產品列表,用戶輸入的搜索文本和單選框的值計算出來。

因此,應用的state是:

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

步驟4:肯定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。

注意:React是單向數據流而且根據組件層級自上而下傳遞的。對於初次使用React的開發者來講可能沒法快速清晰地瞭解什麼組件應該擁有什麼state,因此讓咱們按照下面的步驟來肯定:

對於應用中的每一個state:

  • 找到根據這個state渲染頁面的組件
  • 找到他們共同的全部者組件(在組件層級上高於全部須要這個state的組件)
  • 共同全部者組件或者更高層級的組件擁有這個sate
  • 若是你沒法找到可以合理擁有這個state的組件,那麼就單獨構建一個組件來持有這個state並將它放到層級高於共同全部者的地方

如今讓咱們將上述策略使用在咱們的應用上:

  • ProductTable須要根據state篩選產品列表,SearchBar須要展現用戶輸入和單選框
  • FilterableProductTable是共同全部者
  • 把搜索文本和單選框值放在FilterableProductTable上是合理的

最終,咱們把state放在FilterableProductTable組件中。首先,在FilterableProductTable的構造函數中添加屬性this.state = {filterText: '', inStockOnly: false}來展現應用的初始狀態。以後將filterTextinStockOnly做爲props傳遞給ProductTableSearchBar。最後根據props在ProductTable篩選出對應的產品,在SearchBar中設置相應的值。

如今咱們能夠來看看應用是怎麼表現的了,將filterText的值設爲ball,而後刷新頁面,你就能夠看到產品列表更新了。

步驟5:添加反向數據流

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處更新它的staet。

React顯式地聲明數據以使咱們可以更好地瞭解程序是如何運行的,但這的確比傳統的雙向綁定繁瑣一些。

若是你如今點擊單選框或者在輸入框內輸入,你會發現頁面不會有任何改變,React無視了你的操做。React是故意這麼作的,由於咱們設置了傳遞的prop值與FilterableProductTable中的state值是相同的。

那麼讓咱們來想一想咱們須要讓程序如何運行。咱們想要在用戶輸入時,可以更新state以反應用戶的輸入。由於組件只能更新自身的state,因此FilterableProductTable須要向SearchBar傳遞一個回調函數來實時更新state。咱們可使用<input>標籤上的onChange方法來通知狀態更新。在用戶輸入時,FilterableProductTable傳遞的回調函數將會調用setState()方法來更新應用狀態。

這就是所有了

但願本章可以爲你使用React構建組件和應用提供思路。儘管這相較之前來講須要寫更多的代碼,但請記住,閱讀代碼的時間每每比寫代碼的時間多得多。這樣模塊化,顯式地編寫代碼能讓你更加容易地閱讀代碼。當你開始編寫大型的組件庫時會清晰地意識到代碼模塊化,顯式化的重要性。隨着代碼複用的增多,你的應用的行數也會逐漸減小。

相關文章
相關標籤/搜索