本文是對React官網《Thinking in React》一文的翻譯,經過這篇文章,React團隊向開發者們介紹了應該若是去構思一個web應用,爲從此使用React進行web app的構建,打下基礎。 如下是正文。在咱們團隊看來,React是使用JavaScript構建大型、快速的Web apps的首選方式。它已經在Facebook和Instagram項目中,表現出了很是好的可擴展性。css
可以按照構建的方式來思考web app的實現,是React衆多優勢之一。在這篇文章中,咱們將引導你進行使用React構建可搜索產品數據表的思考過程。html
想象一下,咱們已經有了一個JSON API和來自設計師的設計稿。以下圖所示:react
JSON API返回的數據以下所示:web
[ {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"} ];
咱們要作的第一件事就是給設計稿中的每一個組件(和子組件)畫框,並給它們起名字。若是你正在和一個設計師合做,他可能已經幫你完成了這一步。他的Photoshop圖層名稱可能最終會成爲你的React組件名稱!json
但咱們怎麼知道本身的組件應該是什麼?只須要使用一些通用的技巧來決定是否應該建立一個新的函數或對象。其中一個技巧叫作:單一責任原則。就是說,在理想狀況下,一個組件應該只用來完成一件事。若非如此,則應該考慮將其分解成更小的子組件。數組
咱們常常會向用戶展現JSON數據模型,那麼你應該會發現,若是模型構建正確,那麼你的UI(以及組件結構)應該可以很好地映射數據模型。這是由於UI和數據模型傾向於遵循相同的信息架構,這意味着將UI分解爲組件的工做一般是微不足道的。如今咱們把它分解成映射數據模型的組件以下:架構
如今咱們的示例應用中有了五個組件,並且咱們將每一個組件表明的數據用斜體表示以下:app
細心的你會發現,在ProductTable
中,表頭(包含名稱和價格標籤)不是一個組件。這是一個偏好的問題,有兩個方面的論點。在這個例子中,咱們將其做爲ProductTable
組件的一部分,由於它是ProductTable
負責渲染的數據集的一部分。可是,若是這個頭部變得很複雜(好比咱們要支持排序),那麼將其設置爲ProductTableHeader
這樣的組件確定會更好一些。模塊化
如今咱們已經肯定了設計稿中的組件,下一步咱們要給這些組件安排層次結構。這其實很容易:出如今一個組件中的組件應該在層次結構中顯示爲一個子組件:函數
FilterableProductTable
ProductTable
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具備交互性,須要可以觸發對基礎數據模型的更改。 React使用state讓這一切變得簡單。要正確構建應用程序,首先須要考慮應用程序須要的最小可變狀態集。這裏的關鍵是:不要重複本身。找出應用程序須要的狀態的絕對最小表示,並計算須要的其餘全部內容。例如,若是你正在建立一個TODO列表,只須要保存一個TODO項目的數組;不要爲計數保留一個單獨的狀態變量。相反,當你要渲染TODO數量時,只需取TODO項目數組的長度便可。
考慮咱們示例應用程序中的全部數據。咱們有:
咱們來看看每個是哪個state。這裏有關於每條數據的三個問題:
原來的產品清單是做爲props傳入的,因此這不是state。搜索文本和複選框彷佛是state,由於它們隨着時間而改變,不能從任何東西計算。最後,產品的過濾列表不是state,由於它能夠經過將產品的原始列表與複選框的搜索文本和值組合來計算獲得。
因此最後,咱們的states是:
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:
讓咱們來看看咱們的應用程序的這個策略:
ProductTable
須要根據狀態過濾產品列表,而SearchBar
須要顯示搜索文本和檢查狀態。FilterableProductTable
。FilterableProductTable
中是有意義的酷,因此咱們已經決定,咱們的state存活在FilterableProductTable
中。首先,將一個實例屬性this.state = {filterText:'',inStockOnly:false}
添加到FilterableProductTable
的構造函數中,以反映應用程序的初始狀態。而後,將filterText
和inStockOnly
做爲prop傳遞給ProductTable
和SearchBar
。最後,使用這些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 }