本文梳理了容器與展現組件、高階組件、render props這三類React組件設計模式react
往期回顧:HBaseCon Asia 2019 Track 3 概要回顧git
1、容器(Container)與展現(Presentational)組件github
概念介紹算法
示例編程
咱們來看一個簡單的例子。構造一個組件,該組件的做用是獲取文本並將其展現出來。json
export default class GetText extends React.Component { state = { text: null, } componentDidMount() { fetch('https://api.com/', { headers: { Accept: 'application/json' } }).then(response => { return response.json() }).then(json => { this.setState({ text: json.joke }) }) } render() { return (<div> <div>外部獲取的數據:{this.state.text}</div> <div>UI代碼</div></div> ) }}複製代碼
看到上面 GetText 這個組件,當有和外部數據源進行溝通的邏輯。那麼咱們就能夠把這個組件拆成兩部分。redux
一部分專門負責和外部通訊(容器組件),一部分負責UI邏輯(展現組件)。咱們來將上面那個例子拆分看看。設計模式
容器組件:api
export default class GetTextContainer extends React.Component { state = { text: null, } componentDidMount() { fetch('https://api.com/', { headers: { Accept: 'application/json' } }).then(response => { return response.json() }).then(json => { this.setState({ text: json.joke }) }) } render() { return (<div><GetTextPresentational text={this.state.text}/></div> ) }}複製代碼
展現組件:bash
export default class GetTextPresentational extends React.Component { render() { return (<div> <div>外部獲取的數據:{this.props.text}</div> <div>UI代碼</div></div> ) }}複製代碼
模式所解決的問題
2、高階組件
概念介紹
示例
前面咱們已經說過了,高階組件實際上是利用一個函數,接受 React 組件做爲參數,而後返回新的組件。
咱們這邊新建一個 judgeWoman 函數,接受具體的展現組件,而後判斷是不是女性,
const judgeWoman = (Component) => { const NewComponent = (props) => {// 判斷是不是女性用戶 let isWoman = Math.random() > 0.5 ? true : false if (isWoman) { const allProps = { add: '高階組件增長的屬性', ...props } return <Component {...allProps} />; } else { return <span>女士專用,男士無權瀏覽</span>; } } return NewComponent;};複製代碼
const List = (props) => { return (<div> <div>女士列表頁</div> <div>{props.add}</div></div> )}const WithList = judgeWoman(List)const ShoppingCart = (props) => { return (<div> <div>女士購物頁</div> <div>{props.add}</div></div> )}const WithShoppingCart = judgeWoman(ShoppingCart)複製代碼
const judgeWoman = (Woman,Man) => { const NewComponent = (props) => {// 判斷是不是女性用戶 let isWoman = Math.random() > 0.5 ? true : false if (isWoman) { const allProps = { add: '高階組件增長的屬性', ...props } return <Woman {...allProps} />; } else { return <Man/> } } return NewComponent;};複製代碼
更爲強大的是,因爲函數返回的也是組件,那麼高階組件是能夠嵌套進行使用的!好比咱們先判斷性別,再判斷年齡。
const withComponet =judgeAge(judgeWoman(ShoppingCart))複製代碼
具體代碼可見 src/pattern2(http://t.cn/AiYbYy5g)
模式所解決的問題
一樣的邏輯咱們總不能重複寫屢次。高階組件起到了抽離共通邏輯的做用。同時高階組件的嵌套使用使得代碼複用更加靈活了。
react-redux 就使用了該模式,看到下面的代碼,是否是很熟悉?connect(mapStateToProps, mapDispatchToProps)生成了高階組件函數,該函數接受 TodoList 做爲參數。最後返回了 VisibleTodoList 這個高階組件。
import { connect } from 'react-redux'const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps)(TodoList)複製代碼
使用注意事項
高階組件雖好,咱們使用起來卻要注意以下點。
一、包裝顯示名稱以便輕鬆調試
使用高階組件後 debug 會比較麻煩。當 React 渲染出錯的時候,靠組件的 displayName 靜態屬性來判斷出錯的組件類。HOC 建立的容器組件會與任何其餘組件同樣,會顯示在 React Developer Tools 中。爲了方便調試,咱們須要選擇一個顯示名稱,以代表它是 HOC 的產物。
最多見的方式是用 HOC 包住被包裝組件的顯示名稱。好比高階組件名爲withSubscription,而且被包裝組件的顯示名稱爲 CommentList,顯示名稱應該爲WithSubscription(CommentList):
function withSubscription(WrappedComponent) { class WithSubscription extends React.Component {/* ... */} WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`; return WithSubscription;}function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component';}複製代碼
二、不要在 render 方法中使用 HOC
React 的 diff 算法(稱爲協調)使用組件標識來肯定它是應該更新現有子樹仍是將其丟棄並掛載新子樹。 若是從 render 返回的組件與前一個渲染中的組件相同(===),則 React 經過將子樹與新子樹進行區分來遞歸更新子樹。 若是它們不相等,則徹底卸載前一個子樹。
一般,你不須要考慮這點。但對 HOC 來講這一點很重要,由於這表明着你不該在組件的 render 方法中對一個組件應用 HOC:
render() {// 每次調用 render 函數都會建立一個新的 EnhancedComponent// EnhancedComponent1 !== EnhancedComponent2 const EnhancedComponent = enhance(MyComponent);// 這將致使子樹每次渲染都會進行卸載,和從新掛載的操做! return <EnhancedComponent />;}複製代碼
// 定義靜態函數WrappedComponent.staticMethod = function() {/*...*/}// 如今使用 HOCconst EnhancedComponent = enhance(WrappedComponent);// 加強組件沒有 staticMethodtypeof EnhancedComponent.staticMethod === 'undefined' // true複製代碼
function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/}// 必須準確知道應該拷貝哪些方法 :( Enhance.staticMethod = WrappedComponent.staticMethod; return Enhance;}複製代碼
import hoistNonReactStatic from 'hoist-non-react-statics';function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} hoistNonReactStatic(Enhance, WrappedComponent); return Enhance;}複製代碼
// 使用這種方式代替...MyComponent.someFunction = someFunction;export default MyComponent;// ...單獨導出該方法...export { someFunction };// ...並在要使用的組件中,import 它們import MyComponent, { someFunction } from './MyComponent.js';複製代碼
四、Refs 不會被傳遞
雖然高階組件的約定是將全部 props 傳遞給被包裝組件,但這對於 Refs 並不適用。那是由於 ref 實際上並非一個 prop , 就像 key 同樣,它是由 React 專門處理的。若是將 ref 添加到 HOC 的返回組件中,則 ref 引用指向容器組件,而不是被包裝組件。
這個問題的解決方案是經過使用 React.forwardRef API(React 16.3 中引入)。
3、Render props
概念介紹
示例
具備 render props 的組件預期子組件是一個函數,它所作的就是把子組件當作函數調用,調用參數就是傳入的 props,而後把返回結果渲染出來。
<Provider> {props => <List add={props.add} />}</Provider>複製代碼
咱們具體看下Provider組件是如何定義的。經過這段代碼props.children(allProps),咱們調用了傳入的函數。
const Provider = (props) => {// 判斷是不是女性用戶 let isWoman = Math.random() > 0.5 ? true : false if (isWoman) { const allProps = { add: '高階組件增長的屬性', ...props } return props.children(allProps) } else { return <div>女士專用,男士無權瀏覽</div>; }}複製代碼
好像 render props 能作的高階組件也都能作到啊,並且高階組件更容易理解,是否render props 沒啥用呢?咱們來看一下 render props 更強大的一個點:對於新增的 props 更加靈活。假設咱們的 List 組件接受的是 plus 屬性,ShoppingCart 組件接受的是 add 屬性,咱們能夠直接這樣寫,無需變更 List 組件以及 Provider 自己。使用高階組件達到相同效果就要複雜不少。
<Provider> {props => { const { add } = props return < List plus={add} /> }}</Provider><Provider> {props => <ShoppingCart add={props.add} />}</Provider>複製代碼
const Provider = (props) => {// 判斷是不是女性用戶 let isWoman = Math.random() > 0.5 ? true : false if (isWoman) { const allProps = { add: '高階組件增長的屬性', ...props } return props.test(allProps) } else { return <div>女士專用,男士無權瀏覽</div>; }}const ExampleRenderProps = () => { return ( <div> <Provider test={props => <List add={props.add} />} /> <Provider test={props => <ShoppingCart add={props.add} />} /> </div> )}複製代碼
模式所解決的問題
使用注意事項
將 Render Props 與 React.PureComponent 一塊兒使用時要當心!若是你在 Provider 屬性中建立函數,那麼使用 render props 會抵消使用React.PureComponent 帶來的優點。由於淺比較 props 的時候總會獲得 false,而且在這種狀況下每個 render 對於 render props 將會生成一個新的值。
例如,繼續咱們以前使用的 <List> 組件,若是 List 繼承自 React.PureComponent 而不是 React.Component,咱們的例子看起來就像這樣:
class ExampleRenderProps extends React.Component { render() { return ( <div> {/* 這是很差的! 每一個渲染的 `test` prop的值將會是不一樣的。 */} <Provider test={props => <List add={props.add} />} /> </div> ) }}複製代碼
class ExampleRenderProps extends React.Component { renderList=()=>{ return <List add={props.add} /> } render() { return ( <div> <Provider test={this.renderList} /> </div> ) }}複製代碼
若是你沒法靜態定義 prop(例如,由於你須要關閉組件的 props 和/或 state),則 <List> 應該擴展自React.Component.
小結
React官方文檔
React Component Patterns
React實戰:設計模式和最佳實踐
Presentational and Container Components