在使用JavaScript開發大型、快速的網頁應用時,React是咱們的首選。在Facebook和Instagram,React很好地減小了咱們的工做量。
React最強大部分之一,是讓你在開發應用的同時,按照開發應用的思路去思考。這篇文章,將指導你經過幾個步驟,用React開發一個可搜索數據表html
假設咱們已經有了一個JSON API和一個設計師設計的模型。看起來像這樣:react
咱們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"} ];
首先,在模型圖上框處每一個組件(和子組件),並給組件命名。若是與設計師一塊兒工做,他可能已經心中有數,不妨和他聊聊。他們Photoshop圖層名可能就能成爲你的組件名!github
但如何劃分父組件子組件呢?就像建立一個函數或對象同樣。其中之一是單一職責原則,理想狀況下,一個組件僅作一件事。若是一個組件太大,它應該被分解爲更小的子組件。數組
若是常常把JSON數據模型展示給用戶,你會發現,若模型建立正確,UI(以及組件結構)能很好的反映數據。這是由於UI和數據模型必定程度都是依附於信息架構的,這意味着,將UI分解爲組件並非最重要的,最重要的是,被分解的組件能準確表現你數據模型的一部分。架構
能夠看出,咱們的app有五個組件,咱們用斜體表示每一個組件所表明的數據。app
FilterableProducTable
(可過濾產品表,橙色):包含整個示例模塊化
SearchBar
(搜索欄,藍色):接收用戶輸入函數
ProductTable
(產品表,綠色):根據用戶輸入,展現和過濾數據集測試
ProductCategoryRow
(產品分類行,藍綠色):顯示爲每一個分類的頭部
ProductRow
(產品行,紅色):每一個產品顯示爲一行
觀察ProductTable
,你會發現表頭(包含「Name」和「Price」標籤部分)未被劃分爲一個組件。表頭是否作爲組件屬於我的偏好,至於使用哪一種方式,如下能夠做爲參考。本例中,表頭做爲ProductTable
的一部分,由於表頭屬於渲染的數據集的一部分,因此表頭在ProductTable
的職責範圍內。但若是表頭變得更爲複雜(如爲添加排序功能),將表頭分離爲ProductTableHeader
更合理。
如今,咱們已經將模型劃分爲了組件,接着要安排組件的層次結構。這並不難,模型中一個組件出如今另外一個組件的內部,在層次結構呈現爲它的一個子級。
FilterableProductTable
SearchBar
ProductTable
ProductCategeoryRow
ProductRow
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能夠交互,須要可以觸發數據模型發生改變。React的狀態能讓此變得簡單。
爲了能正確的建立app,你要考慮app須要的最少狀態。關鍵在於不重複,使用最少的狀態,計算出全部你須要的數據。例如,你建立一個TODO列表,只須要保留一個TODO項數組,不須要再使用一個變量來保存TODO項數量,當你須要渲染TODO項數目時,使用TODO項數組長度便可。
考慮示例應用中全部的數據,咱們有:
原始產品列表
用戶輸入的搜索文本
多選框的值
過濾過的產品列表
簡單地問三個問題,讓咱們過一遍,看看哪些是狀態:
是否爲從父組件傳入的屬性?若是是,或許不是狀態
是不是隨時間不變的?若是是,或許不是狀態
是否能經過組件中的其餘狀態或屬性計算出來?若是是,不是狀態
原始產品列表是從屬性傳入的,所以不是狀態。搜索文本和多選框由於會發生變化,且不能經過計算得出,因此是狀態。最後,過濾過的產品列表,能夠經過原始產品列表、搜索文本和多選框值計算出來,所以它不是狀態。
最終,咱們的狀態有:
用戶輸入的搜索文本
多選框的值
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
。首先,在FilterableProductTable
的constructor
中添加示例屬性this.state = {filterText: '', inStockOnly: false}
,來初始化狀態。而後,將filterText
和isStockOnly
做爲屬性傳入ProductTable
和SearchBar
。最後,使用這些屬性過濾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忽略了你的輸入。這是故意的,由於咱們要設置input
的value
屬性要恆等於FilterableProductTable
傳入的狀態。
思考下咱們指望的情況。咱們但願,表單中不管什麼時候發生改變,用戶的輸入要可以更新狀態。由於組件應該只更新本身的狀態,因此FilterableProductTable
會將回調函數傳遞給SearchBar
,SearchBar
會在要更新狀態時調用回調函數。咱們用onChange
事件通知狀態更新。從FilterableProductTable
傳入的回調函數會調用setState()
,從而更新組件。
雖然聽起來有點複雜,可是也就添加幾行代碼。它很是清晰地表現出了app中的數據流動。
但願此文能帶給你使用React建立組件和應用的思路。儘管你可能須要打比平時更多的代碼,但記住,代碼看着老是遠比打着的時候多,而且這些代碼都是模塊化、清晰而易讀的。當你開始開發大型組件庫的時候,你將會慶幸React的數據流清晰和模塊化的特性,而且隨着代碼的複用,代碼規模會開始縮小。