Thinking in React
本文翻譯自React的官方部落格,詳情請閱讀原文。
React非常適合構建元件化的應用,它注重高效能,因此組建的重用,專案的擴充套件都十分靈活,Facebook和instagram的不少商業專案使用了此框架。
本文主要通過“輸入查詢資料”這個簡單的demo來說明或者學習如何用React來架構。
資料模型
我們需要根據JSON API來顯示並且操作資料,最終的視覺化操作是基於JSON資料的基礎之上。最終的效果圖如下:
以下便是我們模擬的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"} ];
step1 變UI為元件繼承
我們如何確定哪一部分應該為一個元件呢?我們可以遵循“單一職責”原則,也就是說,理想的元件只做一件事情。元件也應該根據資料(model)的結構靈活進行設計,這樣最終的UI匹配提供的資料(model),利於維護和理解。
如上圖所示,我們將這個應用分為5個元件。
-
FilterableProductTable
(orange): 包含所有的子元件,是個容器 -
SearchBar
(blue): 用於使用者輸入互動 -
ProductTable
(green): 呈現資料項並根據使用者輸入過濾資料 -
ProductCategoryRow
(turquoise): -
ProductRow
(red): 顯示產品的具體資訊
我們可以看到,tHead部分(Name和Price)並不是一個單獨的元件,在這個例子中,之所以tHead屬於ProductTable元件是因為它並沒有與資料(model)有關聯,考慮這種情況,如果要單擊tHead部分的表頭實現表格內容的排列,我們最好為tHead單獨設計一個元件,並在該元件上繫結事件處理函式。
至此,我們將這五個元件的繼承關係確定下來:
-
FilterableProductTable
SearchBar
-
ProductTable
ProductCategoryRow
ProductRow
step2 建立靜態版本
有了元件的繼承關係,我們首先建立一個靜態版本的應用。我們可以通過元件複用以及父子元件之間的props通訊來完成模型資料的渲染。props是父子元件通訊的一種方式,如果你也瞭解state特性的話,那麼一定不要使用state來構建靜態版本,state用於建立互動版本,也就是說,state中的資料會隨著時間而改變,下面的一節會講解何時將資料放入state中。
我們可以自頂向下或者自下而上來構建應用,在做測試時我們可以自下而上來進行每個模組的測試,而一般構建應用我們則是採用自頂向下的模式,結合資料的自上而下傳遞,利於開發。
在這一步,由於我們構建的是靜態版本,因此每個元件只實現了其render方法,用以基本的資料渲染。最頂層的元件(FilterableProductTable)的props中存入要渲染的資料模型,每當模型資料發生改變時,會對應的檢視層的改變,這也正是React所提出的的單向資料流模型(one-way data flow)。
在React中,元件有兩種型別資料--props和state。它們之間的具體區別可以參考官方文件。
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'}
];
React.render(<FilterableProductTable products={PRODUCTS} />, document.body);
step3 確定元件的state
為了使應用具有動態互動性,必須將狀態的改變(使用者的輸入或者單擊操作等)反映到我們的UI上,通過React給元件提供的state完成上述需求。
我們首先要確定應用的可變的狀態集合,遵循DRY原則:don't repeat youself。我們需要考慮應用中的所有的資料,它包括:
- 基本的產品列表
- 使用者輸入的過濾條件
- checkbox的值
- 過濾後的產品列表
根據下面條件選擇哪些資料可以作為state:
- 是否通過父元件通過props傳遞,如果是,則不是state
- 是否隨著時間而改變,如果不變,則不是state
- 可以通過其他state或者props計算得到,如果可以,則不是state
產品資料列表是通過父元件的props傳遞,因此不是state,使用者輸入和checkbox滿足上述三個條件,可以作為state,二對於過濾的列表,則可以根據產品資料和使用者輸入來獲取到,因此不是state。
故,input輸入值和checkbox的值可以作為state。
step4 確定state所屬的元件
目前確定了state集合,接下來需要確定究竟是哪個元件擁有這個state,或者隨著state而變化。
我們要明確React的單項資料流是沿著元件繼承鏈流動的,這有時很難確定哪一個元件擁有這個state,不過我們可以根據以下原則來大體確定state所屬的元件。
在每一個狀態期,
- 確保每個元件都會根據當前狀態來渲染
- 尋找其共同的祖先元件
- 在繼承鏈中層級較高的元件擁有state
回到我們的應用中,
-
ProductTable需要根據state來過濾資料,SearchBar
需要顯示輸入的文字和選項. - 這兩個元件的共同祖先是
FilterableProductTable
. - 因此state的集合應該所屬
FilterableProductTable元件
所以,我們確定了state所屬的元件是FilterableProductTable。我們需要給該元件設定getInitialState方法設定元件的初始狀態,並且通過props將狀態傳遞給ProductTable和SearchBar,最後我們就可以在ProductTable和SearchBar中獲取狀態並根據當前狀態顯示相應的資料。
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.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;
}.bind(this));
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..." value={this.props.filterText} />
<p>
<input type="checkbox" checked={this.props.inStockOnly} />
{' '}
Only show products in stock
</p>
</form>
);
}
});
var FilterableProductTable = React.createClass({
getInitialState: function() {
return {
filterText: '',
inStockOnly: false
};
},
render: function() {
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'}
];
React.render(<FilterableProductTable products={PRODUCTS} />, document.body);
step5 新增反向資料流
等等,目前構建的應用並不能通過表單來反向設定state,因此,我們無法再input標籤輸入任何值。這就需要我們手動進行反向資料設定。React預設的單項資料流是從model渲染到UI,而通過UI來設定model則需要手動編寫,主要的操作就是通過獲取元件對應的DOM物件,獲取當前DOM的屬性值並反向設定state來完成。
當前版本的應用,React會忽略輸入值和選定值,這是理所當然的,因為我們在FilterableProductTable中設定的state初始值為filterText=‘’,inStockOnly=false,所以對於ProductTable和SearchBar而言,也就是針對這兩個值渲染,但是由於通過input和checkbox的輸入並未改變這兩個state的值,因此,這兩個元件其實並沒有被渲染。
所以我們通過在ProductTable和SearchBar設定事件監聽函式,並且每當函式觸發時setState當前的狀態,促使元件渲染重繪,完成資料的動態呈現。在具體實現中,可以通過refs錨點來獲取具體的具名元件,並通過呼叫元件的getDOMNode方法,獲取對於DOM物件並據此設定新的state。
/** @jsx React.DOM */
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() {
console.log(this.props);
var rows = [];
var lastCategory = null;
this.props.products.forEach(function(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;
}.bind(this));
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
});
var SearchBar = React.createClass({
handleChange: function() {
this.props.onUserInput(
this.refs.filterTextInput.getDOMNode().value,
this.refs.inStockOnlyInput.getDOMNode().checked
);
},
render: function() {
return (
<form>
<input
type="text"
placeholder="Search..."
value={this.props.filterText}
ref="filterTextInput"
onChange={this.handleChange}
/>
<p>
<input
type="checkbox"
checked={this.props.inStockOnly}
ref="inStockOnlyInput"
onChange={this.handleChange}
/>
{' '}
Only show products in stock
</p>
</form>
);
}
});
var FilterableProductTable = React.createClass({
getInitialState: function() {
return {
filterText: '',
inStockOnly: false
};
},
handleUserInput: function(filterText, inStockOnly) {
this.setState({
filterText: filterText,
inStockOnly: inStockOnly
});
},
render: function() {
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'}
];
React.render(<FilterableProductTable products={PRODUCTS} />, document.body);
對,就是這樣
例子雖然非常簡單,但是裡面蘊含的思想確實值得玩味。元件的設計,資料的傳遞,狀態集的確定,雙向資料的傳遞以及事件處理和獲取具名元件等等技術都包含在內,如果真的吃透了這個例子,那麼我想在今後的可重用敏捷開發之路上必定又有新的收穫,具體到我們的實現上就是元件設計的更為優美,程式碼量更為精少。