React思惟方式·譯

在使用JavaScript開發大型、快速的網頁應用時,React是咱們的首選。在Facebook和Instagram,React很好地減小了咱們的工做量。
React最強大部分之一,是讓你在開發應用的同時,按照開發應用的思路去思考。這篇文章,將指導你經過幾個步驟,用React開發一個可搜索數據表html

從一個模型開始

假設咱們已經有了一個JSON API和一個設計師設計的模型。看起來像這樣:react

thinking-in-react-mock

咱們JSON API返回的數據看起來像這樣:git

[
  {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圖層名可能就能成爲你的組件名!github

但如何劃分父組件子組件呢?就像建立一個函數或對象同樣。其中之一是單一職責原則,理想狀況下,一個組件僅作一件事。若是一個組件太大,它應該被分解爲更小的子組件。數組

若是常常把JSON數據模型展示給用戶,你會發現,若模型建立正確,UI(以及組件結構)能很好的反映數據。這是由於UI和數據模型必定程度都是依附於信息架構的,這意味着,將UI分解爲組件並非最重要的,最重要的是,被分解的組件能準確表現你數據模型的一部分。架構

thinking-in-react-components

能夠看出,咱們的app有五個組件,咱們用斜體表示每一個組件所表明的數據。app

  1. FilterableProducTable(可過濾產品表,橙色):包含整個示例模塊化

  2. SearchBar(搜索欄,藍色):接收用戶輸入函數

  3. ProductTable(產品表,綠色):根據用戶輸入,展現和過濾數據集測試

  4. ProductCategoryRow(產品分類行,藍綠色):顯示爲每一個分類的頭部

  5. ProductRow(產品行,紅色):每一個產品顯示爲一行

觀察ProductTable,你會發現表頭(包含「Name」和「Price」標籤部分)未被劃分爲一個組件。表頭是否作爲組件屬於我的偏好,至於使用哪一種方式,如下能夠做爲參考。本例中,表頭做爲ProductTable的一部分,由於表頭屬於渲染的數據集的一部分,因此表頭在ProductTable的職責範圍內。但若是表頭變得更爲複雜(如爲添加排序功能),將表頭分離爲ProductTableHeader更合理。

如今,咱們已經將模型劃分爲了組件,接着要安排組件的層次結構。這並不難,模型中一個組件出如今另外一個組件的內部,在層次結構呈現爲它的一個子級。

  • FilterableProductTable

    • SearchBar

    • ProductTable

      • ProductCategeoryRow

      • ProductRow

第二步:建立React靜態版本

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

class ProductRow extends React.Component {
  render() {
    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>
    );
  }
}

class ProductTable extends React.Component {
  render() {
    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>
    );
  }
}

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>
    );
  }
}


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')
);

如今,你已經有了組件的層次結構,是時候實現這個app了。最簡單的方式,利用數據模型渲染一個無交互的UI版本。建立組件靜態版本須要敲不少鍵盤,無需過多思考,而添加交互效果須要不少思考,不須要敲不少鍵盤,這正是極好的解耦過程。

用數據模型建立app的靜態版本,你在建立組件時會想着複用其它組件,想着用屬性來傳遞數據,而屬性是父級向子級傳遞數據的方式。即使你熟悉狀態的概念,建立靜態版本時也不要用任何狀態。狀態僅用做交互,狀態數據隨時間而變化,由於靜態版本無需任何交互,因此不必使用任何狀態。

在層級結構中,既能夠自上而下開始建立組件(從FilterableProductTable開始),也能夠自下而上開始建立組件(從ProductRow開始)。簡單的例子中,自上而下的方式每每更簡單,對於大型項目來講,自下而上的方式更簡單,更容易測試。

該步完成時,你已經有了一些可複用的組件,這些組件渲染了你的數據模型。靜態版本的組件僅有render()方法。頂層組件(FilterableProductTable)會把數據模型做爲一個屬性。若是數據模型發生變化,再次調用ReactDOM.render(),UI會被更新。你能夠很容易的看出UI是如何被更新的,哪兒發生了改變。React的單向數據流(也叫單向數據綁定),使得一切變得模塊化,變得快速。

簡述:屬性與狀態

屬性和狀態是React中的兩種數據模型,理解二者的區別很是重要。若是你沒法肯定二者的區別,請瀏覽官方文檔

第三步:肯定描述UI的最少狀態

爲了讓UI能夠交互,須要可以觸發數據模型發生改變。React的狀態能讓此變得簡單。

爲了能正確的建立app,你要考慮app須要的最少狀態。關鍵在於不重複,使用最少的狀態,計算出全部你須要的數據。例如,你建立一個TODO列表,只須要保留一個TODO項數組,不須要再使用一個變量來保存TODO項數量,當你須要渲染TODO項數目時,使用TODO項數組長度便可。

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

  • 原始產品列表

  • 用戶輸入的搜索文本

  • 多選框的值

  • 過濾過的產品列表

簡單地問三個問題,讓咱們過一遍,看看哪些是狀態:

  1. 是否爲從父組件傳入的屬性?若是是,或許不是狀態

  2. 是不是隨時間不變的?若是是,或許不是狀態

  3. 是否能經過組件中的其餘狀態或屬性計算出來?若是是,不是狀態

原始產品列表是從屬性傳入的,所以不是狀態。搜索文本和多選框由於會發生變化,且不能經過計算得出,因此是狀態。最後,過濾過的產品列表,能夠經過原始產品列表、搜索文本和多選框值計算出來,所以它不是狀態。

最終,咱們的狀態有:

  • 用戶輸入的搜索文本

  • 多選框的值

第四步:肯定狀態應該放置的位置

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

class ProductRow extends React.Component {
  render() {
    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>
    );
  }
}

class ProductTable extends React.Component {
  render() {
    var rows = [];
    var lastCategory = null;
    this.props.products.forEach((product) => {
      if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
        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() {
    return (
      <form>
        <input type="text" placeholder="Search..." value={this.props.filterText} />
        <p>
          <input type="checkbox" checked={this.props.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>
    );
  }
}


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')
);

OK,咱們已經肯定了app的最少狀態。接下來,咱們要肯定哪一個組件是可變的,或者說狀態屬於哪一個組件。

記住,React在層級及結構中,數據向下流動。這或許不能立刻清楚狀態究竟屬於哪一個組件。這常常也是初學者認識React最有挑戰性的部分。因此,按照如下步驟,弄清這個問題:

對於應用中的每一個狀態:

  • 確認每一個須要這個狀態進行渲染的組件

  • 找到它們共有的父組件(層級結構中,全部須要這個狀態的組件之上的組件)

  • 讓共有父組件,或者比共有父組件更高層級的組件持有該狀態

  • 若是你找不到合適的組件持有該狀態,建立一個新的組件,簡單持有該狀態。把這個組件插入層級結構共有父組件之上

將這個策略用到咱們的應用中:

  • ProductTable要根據狀態過濾產品列表,SearchBar要顯示搜索文本和多選框狀態

  • 共有父組件是FilterableProductTable

  • FilterableProductTable持有過濾文本和多選框值很合理

Cool,就這樣決定把狀態放置到FilterableProductTable。首先,在FilterableProductTableconstructor中添加示例屬性this.state = {filterText: '', inStockOnly: false} ,來初始化狀態。而後,將filterTextisStockOnly做爲屬性傳入ProductTableSearchBar。最後,使用這些屬性過濾ProductTable中的行,和設置SearchBar中表單的值

第五步:添加反向數據流

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

class ProductRow extends React.Component {
  render() {
    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>
    );
  }
}

class ProductTable extends React.Component {
  render() {
    var rows = [];
    var lastCategory = null;
    this.props.products.forEach((product) => {
      if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
        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.handleChange = this.handleChange.bind(this);
  }
  
  handleChange() {
    this.props.onUserInput(
      this.filterTextInput.value,
      this.inStockOnlyInput.checked
    );
  }
  
  render() {
    return (
      <form>
        <input
          type="text"
          placeholder="Search..."
          value={this.props.filterText}
          ref={(input) => this.filterTextInput = input}
          onChange={this.handleChange}
        />
        <p>
          <input
            type="checkbox"
            checked={this.props.inStockOnly}
            ref={(input) => this.inStockOnlyInput = input}
            onChange={this.handleChange}
          />
          {' '}
          Only show products in stock
        </p>
      </form>
    );
  }
}

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

  handleUserInput(filterText, inStockOnly) {
    this.setState({
      filterText: filterText,
      inStockOnly: inStockOnly
    });
  }

  render() {
    return (
      <div>
        <SearchBar
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
          onUserInput={this.handleUserInput}
        />
        <ProductTable
          products={this.props.products}
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
        />
      </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')
);

目前爲止,咱們已經用屬性和狀態,建立了具備在層級結構中至上而下的數據流。如今,是時候支持反向的數據流動,讓底層表單組件可以更新FilterableProductTable中的狀態。

React的單向數據流動使得程序更加清晰易懂,但相比雙向數據綁定,確實須要多打些代碼。

若是你嘗試在當前示例中,輸入文本或選擇多選框,會發現React忽略了你的輸入。這是故意的,由於咱們要設置inputvalue屬性要恆等於FilterableProductTable傳入的狀態。

思考下咱們指望的情況。咱們但願,表單中不管什麼時候發生改變,用戶的輸入要可以更新狀態。由於組件應該只更新本身的狀態,因此FilterableProductTable會將回調函數傳遞給SearchBarSearchBar會在要更新狀態時調用回調函數。咱們用onChange事件通知狀態更新。從FilterableProductTable傳入的回調函數會調用setState(),從而更新組件。

雖然聽起來有點複雜,可是也就添加幾行代碼。它很是清晰地表現出了app中的數據流動。

就這樣

但願此文能帶給你使用React建立組件和應用的思路。儘管你可能須要打比平時更多的代碼,但記住,代碼看着老是遠比打着的時候多,而且這些代碼都是模塊化、清晰而易讀的。當你開始開發大型組件庫的時候,你將會慶幸React的數據流清晰和模塊化的特性,而且隨着代碼的複用,代碼規模會開始縮小。

原文連接

相關文章
相關標籤/搜索