# React 的思維
在我們的主張中,React 是使用 JavaScript 構建大型、快速 Web 應用的第一選擇。它在 Facebook 和 Instagram 中都能很好的應用。
React 中許多偉大的部分中的一個是,它讓你思考如何構建應用。在這個文檔中,我們會帶你了解用 React 構建一個可搜索產品數據表格的思考過程。
## 從一個模擬開始
試想我們已經有一個 JSON API,和從設計者那里得來的一個 模擬。我們的設計者顯然水平一般,因為模擬界面是下面這樣的:

我們的 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"}
];
~~~
## Step 1:打碎 UI 分解為一個組件層級
你要做的第一件事是繪制每個組件的盒子(和子組件),并給它們命名。如果你跟一個設計師一起工作,它們可能已經做了這些,所以去告訴它們!它們的 Photoshop 層名稱可能最終是你的 React 組件的名稱。
但是你如何知道什么會是我們的組件?只要用決定是否應該創建一個新的函數或者對象的技巧就可以了。其中一種方式就是[獨立責任原則](https://en.wikipedia.org/wiki/Single_responsibility_principle),也就是說,一個組件應該在理想情況下只做一件事情。如果最終增加,就應該分解到更小的子組件。
由于經常顯示一個 JSON 數據 模型給用戶,你會發現如果你的模型能夠正確構建,你的 UI(包括你的組件結構)將可以很好的進行映射。這是因為 UI 和數據模型堅守同樣的信息結構,也就是分離你的 UI 到組件通常是比較瑣碎的。只要打碎為表示明確的一部分數據模型的組件即可。

你將看到這里我們有 5 個組件。我們已經為每個組件代表的數據做了斜體標示。
1. FilterableProductTable (orange):包含整個示例
2. SearchBar (blue):接受所有的用戶輸入
3. ProductTable (green): 根據用戶輸入顯示和過濾數據集合
4. ProductCategoryRow (turquoise):對每個類別顯示一個頭
5. ProductRow (red):為每個產品顯示一個行
如果你查看 ProductTable,將會看到這個表頭(包含”Name“ 和”Price“標簽)并不是它自己的組件。這是一個個人喜好,還有一個參數進行制作。對于這個例子,我們使它作為 ProductTable 的一部分,因為它是渲染數據集合的一部分,這是 ProductTable 的責任。
然而,如果 header 變得太復雜(即,如果我們添加用來排序的場景),可能更適用于只做它自己的 ProductTableHeader 組件。
現在我們已經在模擬設計中做了標識,讓我們對它們進行層級排列。這很簡單。在設計中出現在其它組件中的組件,應該作為層級中的子組件:
* FilterableProductTable
* SearchBar
* ProductTable
* ProductCategoryRow
* ProductRow
## Step 2:用 React 構建一個靜態版本
HTML:
~~~
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>
~~~
CSS:
~~~
body {
padding: 5px
}
~~~
BABEL:
~~~
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,用于渲染數據模型,你會想要構建復用其它組件的組件,并使用 props 傳遞數據。props 是從父到子傳遞數據的一種方式。如果你熟悉 state 的概念,在靜態版本構建時不要使用 state 。state 只用于交互,也就是說,數據可以被改變。由于這是一個靜態版本 app,并不需要使用 state 。
可以從上向下或者從下向上構建。也就是說,可以從層級中高層的組件(即 FilterableProductTable)或者使用底層的組件(ProductRow)作為開始。在簡單的例子中,通常從上向下是容易的,而在大型項目,則從下向上較為容易,編寫測試也是。
在這個步驟的結尾,你已經有了一些可復用的組件用來渲染數據模型。組件只有 render() 方法,因為這是靜態版本。層級頂部的組件(FilterableProductTable)將接受你的數據模型作為一個 prop 。如果改變你的底層數據模型并再次調用 ReactDOM.render(),UI 會被更新。很容易看到你的 UI 是如何更新的,包括哪里發生了改變,因為這里并沒有什么復雜的內容。React 的單向數據流(也稱為 one-way binding)使每個部分都能模塊化和快速運行。
如果你需要執行這一步的幫助,可以簡單的參考 [React 文檔](https://facebook.github.io/react/docs/)。
> 小插曲:Props 和 state
React 中有兩種類型的”模型“數據:props 和 state。重要的是理解它們之間的差異;如果你不確定它們之間的區別,參考[官方 React 文檔](https://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html)。
## Step 3:標識最小化(但是完整的)UI 狀態表示
要使你的 UI 可以交互,需要可以觸發底層數據模型的改變。React 通過 state 很容易完成。
要正確構建你的 app ,首先你需要思考你的 app 需要的最小的改變狀態。其中的關鍵是 :不要重復自己(DRY,don't repeat yourself)。想出絕對最小化的狀態表示并計算隨需的所有內容。例如,如果你構建一個 TODO 列表,只要圍繞一個數組的 TODO 項;不要保留一個單獨的狀態變量用于計數。反而,當你希望渲染 TODO 計數,只要簡單的使用 TODO 數組的 length 即可。
思考示例應用中數據的所有部分。我們有:
* 原始的產品列表
* 用戶輸入的搜索文本
* 復選框的值
* 過濾后的產品列表
我們思考每一個,并想想哪個是state 。簡單的問三個問題關于每個數據:
1. 是否從一個父組件中通過 props 傳遞?如果是,它可能不是 state 。
2. 它是否一直不會被改變?如果是,它可能不是 state 。
3. 你可以在你的組件中基于任何其它的 state 或者 props 來計算它嗎?如果是,它可能不是 state 。
原始的產品列表是被作為 props 傳遞,所以它不是 state 。搜索文本和復選框看起來是 state 因為它們會被改變并且不能被從其它內容中計算。最后,過濾后的列表不是 state ,因為它可以通過結合原來的產品列表和搜索文本和復選框的值進行計算。
所以最終,你的 state 是:
* 用戶輸入的搜索文本
* 復選框的值
## Step 4:標識你的 state 應該存在于哪里
BABEL:
~~~
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 是一個單向數據流從層級中自上而下進行。它可能沒有被立即明白哪個組件擁有什么狀態。這是通常對于新用戶最有疑問的部分難于理解,所以跟隨這個步驟來思考:
對于應用中每個部分的 state:
* 標識每個基于這個 state 渲染內容的組件
* 尋找一個通用的擁有者組件(一個單獨的組件,在層級中位于所有需要這個狀態的組件之上)
* 通用的擁有者或者另一個更高級組件擁有這個狀態
* 如果找不出適合擁有這個狀態的組件,創建一個簡單的新組件來保留這個state 并在層級中通用擁有者組件之上添加它。
我們在應用中貫穿這個策略:
* ProductTable 需要基于 state 過濾產品列表,SearchBar 需要顯示 搜索文本和選中狀態 state。
* 通用擁有者組件是 FilterableProductTable。
* 它從概念上講適用于過濾文本和選中狀態值存在于 FilterableProductTable。
那么我們已經決定 state 保存在 FilterableProductTable 中。首先,添加一個實例屬性 this.state = {filterText:'' ,inStockOnly: false} 到 FilterableProductTable 的 構造函數來反映應用的初始狀態。然后,傳遞 filterText 和 inStockOnly 到 ProductTable 和 SearchBar 作為一個 prop 。最后,使用這些 props 來過濾 ProductTable 中的行,并設置 SearchBar 中的表單字段的值。
你可以看一下應用的行為:設置 filterText 為 "ball" 并刷新你的應用。你將發現數據表被正確的更新了。
## Step 5:添加反響數據流
~~~
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')
);
~~~
目前,我們已經構建了一個 app ,正確的渲染為一個層級中的狀態流。現在是時候支持另一種數據流方式:層級深層的 form 組件需要更新 FilterableProductTable 中的 state 。
React 使這個數據流非常明確,來使你容易理解程序如何運行。但是相比傳統的兩種數據綁定來說,的確需要一點額外的輸入。
如果你嘗試 輸入或者選中當前版本示例中的盒子,你發現 React 忽略了你的輸入。這是蓄意的,因為我們已經設置了 input 的 value prop 總是等于從 FilterableProductTable 中傳遞的 state 。
想一下我們希望發生什么。我們要確保無論何時用戶改變了表單,我們更新 state 來反映用戶的輸入。由于組件只能更新它們自己的狀態,FilterableProductTable 將傳遞一個回調到 FilterableProductTable,然后在 state 被更新的時候觸發。我們可以使用 input 的 onChange 事件來關照它。而且被 FilterableProductTable 傳遞的回調 會調用 setState(),然后 app 被更新。
盡管這聽起來很復雜,其實只需要幾行代碼。也很清晰數據在你應用中的流向。
## 就是這樣
希望這給你一個如何使用 React 構建組件和應用的一些想法。雖然可能比你以前接觸到的要多一些輸入,但是記住代碼被閱讀要更重于它的編寫,它非常容易閱讀這個模塊化、明確的代碼。當你開始構建大量組件,你將會開始欣賞這種明確性和模塊化,通過代碼的復用,你的代碼將有效減少。