展現組件 | 容器組件 |
---|---|
關注事物的展現 | 關注事物如何工做 |
可能包含展現和容器組件,而且通常會有DOM標籤和css樣式 | 可能包含展現和容器組件,而且不會有DOM標籤和css樣式 |
經常容許經過this.props.children傳遞 | 提供數據和行爲給容器組件或者展現組件 |
對第三方沒有任何依賴,好比store 或者 flux action | 調用flux action 而且提供他們的回調給展現組件 |
不要指定數據如何加載和變化 | 做爲數據源,一般採用較高階的組件,而不是本身寫,好比React Redux的connect(), Relay的createContainer(), Flux Utils的Container.create() |
僅經過屬性獲取數據和回調 | |
不多有本身的狀態,即便有,也是本身的UI狀態 | |
除非他們須要的本身的狀態,生命週期,或性能優化纔會被寫爲功能組件 |
下面是一個可能會常常寫的組件,評論列表組件,數據交互和展現都放到了一個組件裏面。css
// CommentList.js class CommentList extends React.Component { constructor() { super(); this.state = { comments: [] } } componentDidMount() { $.ajax({ url: "/my-comments.json", dataType: 'json', success: function(comments) { this.setState({comments: comments}); }.bind(this) }); } render() { return <ul> {this.state.comments.map(renderComment)} </ul>; } renderComment({body, author}) { return <li>{body}—{author}</li>; } }
咱們對上面的組件進行拆分,把他拆分紅容器組件 CommentListContainer.js
和展現組件 CommentList
。html
// CommentListContainer.js class CommentListContainer extends React.Component { constructor() { super(); this.state = { comments: [] } } componentDidMount() { $.ajax({ url: "/my-comments.json", dataType: 'json', success: function(comments) { this.setState({comments: comments}); }.bind(this) }); } render() { return <CommentList comments={this.state.comments} />; } } // CommentList.js class CommentList extends React.Component { constructor(props) { super(props); } render() { return <ul> {this.props.comments.map(renderComment)} </ul>; } renderComment({body, author}) { return <li>{body}—{author}</li>; } }
優點:react
UI
state
數據源UI
下面是一個最簡單的無狀態組件的例子:ajax
function HelloComponent(props, /* context */) { return <div>Hello {props.name}</div> } ReactDOM.render(<HelloComponent name="Sebastian" />, mountNode)
能夠看到,本來須要寫「類」定義(React.createClass
或者 class YourComponent extends React.Component
)來建立本身組件的定義(有狀態組件),如今被精簡成了只寫一個 render
函數。更值得一提的是,因爲僅僅是一個無狀態函數,React
在渲染的時候也省掉了將「組件類」 實例化的過程。算法
結合 ES6
的解構賦值,可讓代碼更精簡。例以下面這個 Input
組件:json
function Input({ label, name, value, ...props }, { defaultTheme }) { const { theme, autoFocus, ...rootProps } = props return ( <label htmlFor={name} children={label || defaultLabel} {...rootProps} > <input name={name} type="text" value={value || ''} theme={theme || defaultTheme} {...props} /> )} Input.contextTypes = {defaultTheme: React.PropTypes.object};
無狀態組件不像上述兩種方法在調用時會建立新實例,它建立時始終保持了一個實例,避免了沒必要要的檢查和內存分配,作到了內部優化。數組
無狀態組件不支持 "ref"瀏覽器
高階組件經過函數和閉包,改變已有組件的行爲,本質上就是 Decorator
模式在 React
的一種實現。性能優化
當寫着寫着無狀態組件的時候,有一天突然發現須要狀態處理了,那麼無需完全返工:)
每每咱們須要狀態的時候,這個需求是能夠重用的。閉包
高階組件加無狀態組件,則大大加強了整個代碼的可測試性和可維護性。同時不斷「誘使」咱們寫出組合性更好的代碼。
function welcome() { let username = localStorage.getItem('username'); console.log('welcome ' + username); } function goodbey() { let username = localStorage.getItem('username'); console.log('goodbey ' + username); } welcome(); goodbey();
咱們發現兩個函數有一句代碼是同樣的,這叫冗餘唉。(平時可能會有一大段代碼的冗餘)。
下面咱們要寫一箇中間函數,讀取username,他來負責把username傳遞給兩個函數。
function welcome(username) { console.log('welcome ' + username); } function goodbey(username) { console.log('goodbey ' + username); } function wrapWithUsername(wrappedFunc) { let newFunc = () => { let username = localStorage.getItem('username'); wrappedFunc(username); }; return newFunc; } welcome = wrapWithUsername(welcome); goodbey = wrapWithUsername(goodbey); welcome(); goodbey();
好了,咱們裏面的 wrapWithUsername
函數就是一個「高階函數」。
他作了什麼?他幫咱們處理了 username
,傳遞給目標函數。咱們調用最終的函數 welcome
的時候,根本不用關心 username
是怎麼來的。
下面是兩個冗餘的組件。
import React, {Component} from 'react' class Welcome extends Component { constructor(props) { super(props); this.state = { username: '' } } componentWillMount() { let username = localStorage.getItem('username'); this.setState({ username: username }) } render() { return ( <div>welcome {this.state.username}</div> ) } } export default Welcome;
import React, {Component} from 'react' class Goodbye extends Component { constructor(props) { super(props); this.state = { username: '' } } componentWillMount() { let username = localStorage.getItem('username'); this.setState({ username: username }) } render() { return ( <div>goodbye {this.state.username}</div> ) } } export default Goodbye;
咱們能夠經過剛剛高階函數的思想來建立一箇中間組件,也就是咱們說的高階組件。
import React, {Component} from 'react' export default (WrappedComponent) => { class NewComponent extends Component { constructor() { super(); this.state = { username: '' } } componentWillMount() { let username = localStorage.getItem('username'); this.setState({ username: username }) } render() { return <WrappedComponent username={this.state.username}/> } } return NewComponent }
import React, {Component} from 'react'; import wrapWithUsername from 'wrapWithUsername'; class Welcome extends Component { render() { return ( <div>welcome {this.props.username}</div> ) } } Welcome = wrapWithUsername(Welcome); export default Welcome;
import React, {Component} from 'react'; import wrapWithUsername from 'wrapWithUsername'; class Goodbye extends Component { render() { return ( <div>goodbye {this.props.username}</div> ) } } Goodbye = wrapWithUsername(Goodbye); export default Goodbye;
看到沒有,高階組件就是把 username
經過 props
傳遞給目標組件了。目標組件只管從 props
裏面拿來用就行了。
爲了代碼的複用性,咱們應該儘可能減小代碼的冗餘。
使用react時,組件或容器的代碼在根本上必須只負責一塊UI功能。
若是組件根本不須要狀態,那麼就使用函數定義的無狀態組件。
從性能上來講,函數定義的無狀態組件 > ES6 class
定義的組件 > 經過 React.createClass()
定義的組件。
僅傳遞組件所須要的屬性。只有當屬性列表太長時,才使用{...this.props}
進行傳遞。
若是組件裏面有太多的判斷邏輯(if-else
語句)一般意味着這個組件須要被拆分紅更細的組件或模塊。
使用明確的命名可以讓開發者明白它的功能,有助於組件複用。
在shouldComponentUpdate
中避免沒必要要的檢查.
儘可能使用不可變數據類型(Immutable
).
編寫針對產品環境的打包配置(Production Build
).
經過Chrome Timeline
來記錄組件所耗費的資源.
在componentWillMount
或者componentDidMount
裏面經過setTimeOut
或者requestAnimationFram
來延遲執行那些須要大量計算的任務.
在大多數狀況下,咱們推薦使用受控組件來實現表單。在受控組件中,表單數據由 React 組件負責處理。下面是一個典型的受控組建。
class NameForm extends React.Component { constructor(props) { super(props); this.state = {value: ''}; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } handleChange(event) { this.setState({value: event.target.value}); } handleSubmit(event) { alert('A name was submitted: ' + this.state.value); event.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <label> <input type="text" value={this.state.value} onChange={this.handleChange} /> </label> <input type="submit" value="Submit" /> </form> ); } }
設置表單元素的value
屬性以後,其顯示值將由this.state.value
決定,以知足React
狀態的同一數據理念。每次鍵盤敲擊以後會執行handleChange
方法以更新React
狀態,顯示值也將隨着用戶的輸入改變。
對於受控組件來講,每一次 state
(狀態)變化都會伴有相關聯的處理函數。這使得能夠直接修改或驗證用戶的輸入和提交表單。
由於不受控組件的數據來源是 DOM 元素,當使用不受控組件時很容易實現 React 代碼與非 React 代碼的集成。若是你但願的是快速開發、不要求代碼質量,不受控組件能夠必定程度上減小代碼量。不然。你應該使用受控組件。
通常狀況下不受控組件咱們使用ref
來獲取DOM
元素進行操做。
class NameForm extends React.Component { constructor(props) { super(props); this.handleSubmit = this.handleSubmit.bind(this); } handleSubmit(event) { alert('A name was submitted: ' + this.input.value); event.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <label> Name: <input type="text" ref={(input) => this.input = input} /> </label> <input type="submit" value="Submit" /> </form> ); } }
const sampleComponent = () => { return isTrue ? <p>True!</p> : <p>false!</p> };
const sampleComponent = () => { return isTrue ? <p>True!</p> : <none/> };
const sampleComponent = () => { return isTrue && <p>True!</p> };
須要注意的是若是isTrue
爲 0 ,其實會轉換成 false
,可是在頁面中顯示的時候,&&
仍是會返回0
顯示到頁面中。
// 問題代碼 const sampleComponent = () => { return ( <div> {flag && flag2 && !flag3 ? flag4 ? <p>Blah</p> : flag5 ? <p>Meh</p> : <p>Herp</p> : <p>Derp</p> } </div> ) };
解決方案:
const sampleComponent = () => { const basicCondition = flag && flag2 && !flag3; if (!basicCondition) return <p>Derp</p>; if (flag4) return <p>Blah</p>; if (flag5) return <p>Meh</p>; return <p>Herp</p> }
在某些狀況下,React
框架出於性能優化考慮,可能會將屢次state
更新合併成一次更新。正由於如此,setState
其實是一個異步的函數。 若是在調用setState()
函數以後嘗試去訪問this.state
,你獲得的可能仍是setState()
函數執行以前的結果。
可是,有一些行爲也會阻止React
框架自己對於屢次state
更新的合併,從而讓state
的更新變得同步化。 好比: eventListeners
, Ajax
, setTimeout
等等。
React
框架之因此在選擇在調用setState
函數以後當即更新state而不是採用框架默認的方式,即合併屢次state
更新爲一次更新,是由於這些函數調用(fetch
,setTimeout
等瀏覽器層面的API
調用)並不處於React
框架的上下文中,React
沒有辦法對其進行控制。React
在此時採用的策略就是及時更新,確保在這些函數執行以後的其餘代碼能拿到正確的數據(即更新過的state
)。
根據React
官方文檔,setState
函數實際上接收兩個參數,其中第二個參數類型是一個函數,做爲setState
函數執行後的回調。經過傳入回調函數的方式,React
能夠保證傳入的回調函數必定是在setState
成功更新this.state
以後再執行。
this.setState({count: 1}, () => { console.log(this.state.count); // 1 })
ReactComponent.prototype.setState = function(partialState, callback) { invariant( typeof partialState === 'object' || typeof partialState === 'function' || partialState == null, 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.' ); this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); } };
updater
的這兩個方法,和React
底層的Virtual Dom
(虛擬DOM樹)的diff
算法有緊密的關係,因此真正決定同步仍是異步的實際上是Virtual DOM
的diff
算法。
在React
中,想作依賴注入(Dependency Injection
)其實至關簡單。能夠經過props
來進行傳遞。可是,若是組件數量不少,而且組件嵌套層次很深的話,這種方式就不太合適。
// inject.jsx var title = 'React Dependency Injection'; export default function inject(Component) { return class Injector extends React.Component { render() { return ( <Component {...this.state} {...this.props} title={ title } /> ) } }; }
// Title.jsx export default function Title(props) { return <h1>{ props.title }</h1>; }
// Header.jsx import inject from './inject.jsx'; import Title from './Title.jsx'; var EnhancedTitle = inject(Title); export default function Header() { return ( <header> <EnhancedTitle /> </header> ); }
React v16.3.0
以前的 Context
:
var context = { title: 'React in patterns' }; class App extends React.Component { getChildContext() { return context; } // ... } App.childContextTypes = { title: PropTypes.string };
class Inject extends React.Component { render() { var title = this.context.title; // ... } } Inject.contextTypes = { title: PropTypes.string };
以前的 Context
做爲一個實驗性質的 API
,直到 React v16.3.0
版本前都一直不被官方所提倡去使用,其主要緣由就是由於在子組件中使用 Context
會破壞 React
應用的分型架構。
這裏的分形架構指的是從理想的 React
應用的根組件樹中抽取的任意一部分都還是一個能夠直接運行的子組件樹。在這個子組件樹之上再包一層,就能夠將它無縫地移植到任意一個其餘的根組件樹中。
但若是根組件樹中有任意一個組件使用了支持透傳的 Context
API
,那麼若是把包含了這個組件的子組件樹單獨拿出來,由於缺乏了提供 Context
值的根組件樹,這時的這個子組件樹是沒法直接運行的。
而且他有一個致命缺陷:任何一箇中間傳遞的組件shouldComponentUpdate
函數返回false
,組件都不會獲得更新。
新的Context Api
新的Context Api
採用聲明式的寫法,而且能夠透過shouldComponentUpdate
函數返回false
的組件繼續向下傳播,以保證目標組件必定能夠接收到頂層組件 Context
值的更新,一舉解決了現有 Context API
的兩大弊端,也終於成爲了 React
中的第一級(first-class) API
。
新的 Context API 分爲三個組成部分:
React.createContext
用於初始化一個 Context
。
XXXContext.Provider
做爲頂層組件接收一個名爲 value
的 prop
,能夠接收任意須要被放入 Context
中的字符串,數字,甚至是函數。
XXXContext.Consumer
做爲目標組件能夠出如今組件樹的任意位置(在 Provider
以後),接收 children prop
,這裏的 children
必須是一個函數(context => ()
)用來接收從頂層傳來的 Context
。
const ThemeContext = React.createContext('light'); class App extends React.Component { render() { return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } } function Toolbar(props) { return ( <div> <ThemedButton /> </div> ); } function ThemedButton(props) { return ( <ThemeContext.Consumer> {theme => <Button {...props} theme={theme} />} </ThemeContext.Consumer> ); }
class Switcher extends React.Component { constructor(props) { super(props); this.state = { name: 'React in patterns' }; } render() { return ( <button onClick={ this._handleButtonClick }> click me </button> ); } _handleButtonClick() { console.log(`Button is clicked inside ${ this.state.name }`); // 將致使 // Uncaught TypeError: Cannot read property 'state' of null } }
咱們能夠經過下面三種方式簡單實現this指向的綁定:
constructor
中事先綁定 this._buttonClick = this._handleButtonClick.bind(this);
<button onClick={ () => this._buttonClick() }>
<button onClick={ ::this._buttonClick() }>
setState() 不只能接受一個對象,還能接受一個函數做爲參數呢,該函數接受該組件前一刻的 state 以及當前的 props 做爲參數,計算和返回下一刻的 state。
// assuming this.state.count === 0 this.setState({count: this.state.count + 1}); this.setState({count: this.state.count + 1}); this.setState({count: this.state.count + 1}); // this.state.count === 1, not 3 this.setState((prevState, props) => ({ count: prevState.count + props.increment }));
// Passing object this.setState({ expanded: !this.state.expanded }); // Passing function this.setState(prevState => ({ expanded: !prevState.expanded }));
import HomePage from './HomePage.jsx'; import AboutPage from './AboutPage.jsx'; import UserPage from './UserPage.jsx'; import FourOhFourPage from './FourOhFourPage.jsx'; const PAGES = { home: HomePage, about: AboutPage, user: UserPage }; const Page = (props) => { const Handler = PAGES[props.page] || FourOhFourPage; return <Handler {...props} /> };
基礎組件, 佈局組件, 排版組件
請保持樣式遠離那些離不開state的組件. 好比路由, 視圖, 容器, 表單, 佈局等等不該該有任何的樣式或者css class出如今組件上. 相反, 這些複雜的業務組件應該有一些帶有基本功能的無狀態UI組件組成.
class SampleComponent extends Component { render() { return ( <form onSubmit={this.handleSubmit}> <Heading children='Sign In'/> <Input name='username' value={username} onChange={this.handleChange}/> <Input type='password' name='password' value={password} onChange={this.handleChange}/> <Button type='submit' children='Sign In'/> </form> ) } } // 表達組件(帶樣式) const Button = ({ ...props }) => { const sx = { fontFamily: 'inherit', fontSize: 'inherit', fontWeight: 'bold', textDecoration: 'none', display: 'inline-block', margin: 0, paddingTop: 8, paddingBottom: 8, paddingLeft: 16, paddingRight: 16, border: 0, color: 'white', backgroundColor: 'blue', WebkitAppearance: 'none', MozAppearance: 'none' } return ( <button {...props} style={sx}/> ) }
通常來講, 在組件內寫死(hard code)樣式應該是要被避免的. 這些有可能被不一樣的UI組件分享的樣式應該被分開放入對應的模塊中.
// 樣式模塊 export const white = '#fff'; export const black = '#111'; export const blue = '#07c'; export const colors = { white, black, blue }; export const space = [ 0, 8, 16, 32, 64 ]; const styles = { bold: 600, space, colors }; export default styles
// button.jsx import React from 'react' import { bold, space, colors } from './styles' const Button = ({ ...props }) => { const sx = { fontFamily: 'inherit', fontSize: 'inherit', fontWeight: bold, textDecoration: 'none', display: 'inline-block', margin: 0, paddingTop: space[1], paddingBottom: space[1], paddingLeft: space[2], paddingRight: space[2], border: 0, color: colors.white, backgroundColor: colors.blue, WebkitAppearance: 'none', MozAppearance: 'none' }; return ( <button {...props} style={sx}/> ) };
// Modular powers of two scale const scale = [ 0, 8, 16, 32, 64 ]; // 經過這個函數去取得一部分的樣式 const createScaledPropertyGetter = (scale) => (prop) => (x) => { return (typeof x === 'number' && typeof scale[x] === 'number') ? {[prop]: scale[x]} : null }; const getScaledProperty = createScaledPropertyGetter(scale); export const getMargin = getScaledProperty('margin'); export const getPadding = getScaledProperty('padding'); // 樣式函數的用法 const Box = ({ m, p, ...props }) => { const sx = { ...getMargin(m), ...getPadding(p) }; return <div {...props} style={sx}/> }; // 組件用法. const Box = () => ( <div> <Box m={2} p={3}> A box with 16px margin and 32px padding </Box> </div> );
class SampleComponent extends Component { // constructor function (or getInitialState) constructor(props) { super(props); this.state = { flag: false, inputVal: props.inputValue }; } render() { return <div>{this.state.inputVal && <AnotherComponent/>}</div> } }
這樣作的危險在於, 有可能組件的props
發生了改變可是組件卻沒有被更新. 新的props
的值不會被React
認爲是更新的數據由於構造器constructor
或者getInitialState
方法在組件建立以後不會再次被調用了,所以組件的state
再也不會被更新。 要記住, State
的初始化只會在組件第一次初始化的時候發生。
class SampleComponent extends Component { // constructor function (or getInitialState) constructor(props) { super(props); this.state = { flag: false }; } render() { return <div>{this.props.inputValue && <AnotherComponent/>}</div> } }
更乾淨的render
函數? 這個概念可能會有點讓人疑惑.
其實在這裏乾淨是指咱們在shouldComponentUpdate
這個生命週期函數裏面去作淺比較, 從而避免沒必要要的渲染.
class Table extends PureComponent { render() { return ( <div> {this.props.items.map(i => <Cell data={i} options={this.props.options || []}/> )} </div> ); } }
這種寫法的問題在於{this.props.options || []}
這種寫法會致使全部的Cell
都被從新渲染即便只有一個cell
發生了改變. 爲何會發生這種事呢?
仔細觀察你會發現, options
這個數組被傳到了Cell
這個組件上, 通常狀況下, 這不會致使什麼問題. 由於若是有其餘的Cell
組件, 組件會在有props
發生改變的時候淺對比props
而且跳過渲染(由於對於其餘Cell
組件, props
並無發生改變). 可是在這個例子裏面, 當options
爲null
時, 一個默認的空數組就會被當成Props
傳到組件裏面去. 事實上每次傳入的[]
都至關於建立了新的Array
實例. 在JavaScript
裏面, 不一樣的實例是有不一樣的實體的, 因此淺比較在這種狀況下老是會返回false
, 而後組件就會被從新渲染. 由於兩個實體不是同一個實體. 這就徹底破壞了React
對於咱們組件渲染的優化.
const defaultval = []; // <--- 也可使用defaultProps class Table extends PureComponent { render() { return ( <div> {this.props.items.map(i => <Cell data={i} options={this.props.options || defaultval}/> )} </div> ); } }
class App extends PureComponent { render() { return <MyInput onChange={e => this.props.update(e.target.value)}/>; } }
class App extends PureComponent { update(e) { this.props.update(e.target.value); } render() { return <MyInput onChange={this.update.bind(this)}/>; } }
在上面的兩個壞實踐中, 每次咱們都會去建立一個新的函數實體. 和第一個例子相似, 新的函數實體會讓咱們的淺比較返回false
, 致使組件被從新渲染. 因此咱們須要在更早的時候去bind
咱們的函數.
class App extends PureComponent { constructor(props) { super(props); this.update = this.update.bind(this); } update(e) { this.props.update(e.target.value); } render() { return <MyInput onChange={this.update}/>; } }
React模塊名使用帕斯卡命名,實例使用駱駝式命名
// bad import reservationCard from './ReservationCard'; // good import ReservationCard from './ReservationCard'; // bad const ReservationItem = <ReservationCard />; // good const reservationItem = <ReservationCard />;
// bad export default function withFoo(WrappedComponent) { return function WithFoo(props) { return <WrappedComponent {...props} foo />; } } // good export default function withFoo(WrappedComponent) { function WithFoo(props) { return <WrappedComponent {...props} foo />; } const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; WithFoo.displayName = `withFoo(${wrappedComponentName})`; return WithFoo; }
避免使用DOM相關的屬性來用做其餘的用途。
// bad <MyComponent style="fancy" /> // good <MyComponent variant="fancy" />
在React模塊中,不要給所謂的私有函數添加 _ 前綴,本質上它並非私有的。
爲何?_
下劃線前綴在某些語言中一般被用來表示私有變量或者函數。可是不像其餘的一些語言,在JS中沒有原生支持所謂的私有變量,全部的變量函數都是共有的。儘管你的意圖是使它私有化,在以前加上下劃線並不會使這些變量私有化,而且全部的屬性(包括有下劃線前綴及沒有前綴的)都應該被視爲是共有的。
class extends React.Component 的生命週期函數:
可選的 static 方法
點擊回調或者事件處理器 如 onClickSubmit()
或 onChangeDescription()
render
裏的 getter
方法 如 getSelectReason()
或 getFooterContent()
可選的 render
方法 如 renderNavigation()
或 renderProfilePicture()
render
render()
方法
如何定義 propTypes
, defaultProps
, contextTypes
, 等等其餘屬性...
import React from 'react'; import PropTypes from 'prop-types'; const propTypes = { id: PropTypes.number.isRequired, url: PropTypes.string.isRequired, text: PropTypes.string, }; const defaultProps = { text: 'Hello World', }; class Link extends React.Component { static methodsAreOk() { return true; } render() { return <a href={this.props.url} data-id={this.props.id}>{this.props.text}</a>; } } Link.propTypes = propTypes; Link.defaultProps = defaultProps; export default Link;