前言:對很多 react 新手來說,網上能找到的資源大都是些簡單的 tutorial ,它們能教會你如何使用 react ,但並不會告訴你怎么在實際項目中優雅的組織和編寫 react 代碼。用谷歌搜中文“ React 最佳實踐”發現前兩頁幾乎全都是同一篇國外文章的譯文...所以我總結了下自己過去那個項目使用 React 踩過的一些坑,也整理了一些別人的觀點,希望對部分 react 使用者有幫助。
React 與 AJAX
React只負責處理View這一層,它本身不涉及網絡請求/AJAX,所以這里我們需求考慮兩個問題:
-
第一,用什么技術從服務端獲取數據;
-
第二,獲取到的數據應該放在react組件的什么位置。
React官方提供了一種解決方案:Load Initial Data via AJAX
使用jQuery的Ajax方法,在一個組件的componentDidMount()
中發ajax請求,拿到的數據存在組件自己的state
中,並調用setState
方法去更新UI。如果是異步獲取數據,則在componentWillUnmount
中取消發送請求。
如果只是為了使用jQuery的Ajax方法就引入整個jQuery庫,既是一種浪費又加大了整個應用的體積。那我們還有什么其他的選擇嗎?事實上是有很多的:fetch()、fetch polyfill、axios...其中最需要我們關注的是window.fetch()
,它是一個簡潔、標准化的javascript的Ajax API。在Chrome和Firefox中已經可以使用,如果需要兼容其他瀏覽器,可以使用fetch polyfill。
React官方文檔只告訴了我們在一個單一組件中如何通過ajax從服務器端獲取數據,但並沒有告訴我們在一個完整的實際項目中到底應該把數據存在哪些組件中,這部分如果缺乏規范的話,會導致整個項目變得混亂、難以維護。下面給出三種比較好的實踐:
1. 所有的數據請求和管理都存放在唯一的一個根組件
讓父組件/根組件集中發送所有的ajax請求,把從服務端獲取的數據統一存放在這個組件的state中,再通過props把數據傳給子組件。這種方法主要是針對組件樹不是很復雜的小型應用。缺點就是當組件樹的層級變多了以后,需要把數據一層一層地傳給子組件,寫起來麻煩,性能也不好。
2. 設置多個容器組件專門處理數據請求和管理
其實跟第一種方法類似,只不過設置多個容器組件來負責數據請求和狀態管理。這里我們需要區分兩種不同類型的組件,一種是展示性組件(presentational component),另一種是容器性組件(container component)。展示性組件本身不擁有任何狀態,所有的數據都從容器組件中獲得,在容器組件中發送ajax請求。兩者更詳細的描述,可以閱讀下這篇文章:Presentational and Container Components
一個具體的例子:
假設我們需要展示用戶的姓名和頭像,首先創建一個展示性組件<UserProfile />
,它接受兩個Props:name
和profileImage
。這個組件內部沒有任何關於Ajax的代碼。
然后創建一個容器組件<UserProfileContainer />
,它接受一個userId
的參數,發送Ajax請求從服務器獲取數據存在state
中,再通過props
傳給<UserProfile />
組件。
3. 使用Redux或Relay的情況
Redux管理狀態和數據,Ajax從服務器端獲取數據,所以很顯然當我們使用了Redux時,應該把所有的網絡請求都交給redux來解決。具體來說,應該是放在Async Actions。如果用其他類Flux庫的話,解決方式都差不多,都是在actions中發送網絡請求。
Relay是Facebook官方推出的一個庫。如果用它的話,我們只需要通過GraphQL來聲明組件需要的數據,Relay會自動地把下載數據並通過props往下傳遞。不過想要用Relay,你得先有一個GraphQL的服務器...
一個標准組件的組織結構
1 class definition 1.1 constructor 1.1.1 event handlers 1.2 'component' lifecycle events 1.3 getters 1.4 render 2 defaultProps 3 proptypes
示例:
class Person extends React.Component { constructor (props) { super(props); this.state = { smiling: false }; this.handleClick = () => { this.setState({smiling: !this.state.smiling}); }; } componentWillMount () { // add event listeners (Flux Store, WebSocket, document, etc.) }, componentDidMount () { // React.getDOMNode() }, componentWillUnmount () { // remove event listeners (Flux Store, WebSocket, document, etc.) }, get smilingMessage () { return (this.state.smiling) ? "is smiling" : ""; } render () { return ( <div onClick={this.handleClick}> {this.props.name} {this.smilingMessage} </div> ); }, } Person.defaultProps = { name: 'Guest' }; Person.propTypes = { name: React.PropTypes.string };
以上示例代碼的來源:https://github.com/planningcenter/react-patterns#component-organization
使用 PropTypes 和 getDefaultProps()
-
一定要寫PropTypes,切莫為了省事而不寫
-
如果一個Props不是requied,一定在getDefaultProps中設置它
React.PropTypes
主要用來驗證組件接收到的props是否為正確的數據類型,如果不正確,console中就會出現對應的warning。出於性能方面的考慮,這個API只在開發環境下使用。
基本使用方法:
propTypes: {
myArray: React.PropTypes.array, myBool: React.PropTypes.bool, myFunc: React.PropTypes.func, myNumber: React.PropTypes.number, myString: React.PropTypes.string, // You can chain any of the above with `isRequired` to make sure a warning // is shown if the prop isn't provided. requiredFunc: React.PropTypes.func.isRequired }
假如我們props不是以上類型,而是擁有復雜結構的對象怎么辦?比如下面這個:
{ text: 'hello world', numbers: [5, 2, 7, 9], }
當然,我們可以直接用React.PropTypes.object
,但是對象內部的數據我們卻無法驗證。
propTypes: { myObject: React.PropTypes.object, }
進階使用方法:shape()
和 arrayOf()
propTypes: { myObject: React.PropTypes.shape({ text: React.PropTypes.string, numbers: React.PropTypes.arrayOf(React.PropTypes.number), }) }
下面是一個更復雜的Props:
[
{
name: 'Zachary He', age: 13, married: true, }, { name: 'Alice Yo', name: 17, }, { name: 'Jonyu Me', age: 20, married: false, } ]
綜合上面,寫起來應該就不難了:
propTypes: { myArray: React.PropTypes.arrayOf( React.propTypes.shape({ name: React.propTypes.string.isRequired, age: React.propTypes.number.isRequired, married: React.propTypes.bool }) ) }
把計算和條件判斷都交給 render()
方法吧
1. 組件的state中不能出現props
// BAD: constructor (props) { this.state = { fullName: `${props.firstName} ${props.lastName}` }; } render () { var fullName = this.state.fullName; return ( <div> <h2>{fullName}</h2> </div> ); }
// GOOD: render () { var fullName = `${this.props.firstName} ${this.props.lastName}`; }
當然,復雜的display logic也應該避免全堆放在render()中,因為那樣可能導致整個render()方法變得臃腫,不優雅。我們可以把一些復雜的邏輯通過helper function移出去。
// GOOD: helper function renderFullName () { return `${this.props.firstName} ${this.props.lastName}`; } render () { var fullName = this.renderFullName(); }
2. 保持state的簡潔,不要出現計算得來的state
// WRONG: constructor (props) { this.state = { listItems: [1, 2, 3, 4, 5, 6], itemsNum: this.state.listItems.length }; } render() { return ( <div> <span>{this.state.itemsNum}</span> </div> ) }
// Right: render () { var itemsNum = this.state.listItems.length; }
3. 能用三元判斷符,就不用If,直接放在render()里
// BAD: renderSmilingStatement () { if (this.state.isSmiling) { return <span>is smiling</span>; }else { return ''; } }, render () { return <div>{this.props.name}{this.renderSmilingStatement()}</div>; }
// GOOD: render () { return ( <div> {this.props.name} {(this.state.smiling) ? <span>is smiling</span> : null } </div> ); }
4. 布爾值都不能搞定的,交給IIFE吧
Immediately-invoked function expression
return ( <section> <h1>Color</h1> <h3>Name</h3> <p>{this.state.color || "white"}</p> <h3>Hex</h3> <p> {(() => { switch (this.state.color) { case "red": return "#FF0000"; case "green": return "#00FF00"; case "blue": return "#0000FF"; default: return "#FFFFFF"; } })()} </p> </section> );
5. 不要把display logic寫在componentWillReceiveProps
或componentWillMount
中,把它們都移到render()中去。
如何動態處理 classNames
1. 使用布爾值
// BAD:
constructor () {
this.state = { classes: [] }; } handleClick () { var classes = this.state.classes; var index = classes.indexOf('active'); if (index != -1) { classes.splice(index, 1); } else { classes.push('active'); } this.setState({ classes: classes }); }
// GOOD: constructor () { this.state = { isActive: false }; } handleClick () { this.setState({ isActive: !this.state.isActive }); }
2. 使用classnames這個小工具來拼接classNames:
// BEFORE: var Button = React.createClass({ render () { var btnClass = 'btn'; if (this.state.isPressed) btnClass += ' btn-pressed'; else if (this.state.isHovered) btnClass += ' btn-over'; return <button className={btnClass}>{this.props.label}</button>; } });
// AFTER: var classNames = require('classnames'); var Button = React.createClass({ render () { var btnClass = classNames({ 'btn': true, 'btn-pressed': this.state.isPressed, 'btn-over': !this.state.isPressed && this.state.isHovered }); return <button className={btnClass}>{this.props.label}</button>; } });
未完待續...