本身在總結最近半年的React開發最佳實踐時,提到了Render Props,想好好寫寫,但感受篇幅又太長,因此就有了此文。願你看完,能有所收穫,若是有什麼不足或錯誤之處還請指正。文中所提到的全部代碼均可以在示例項目中找到,並npm i,npm start跑起來:
Github:示例項目html
react官方文檔中是這樣描述Render Props的:術語 「render prop」 是指一種在 React 組件之間使用一個值爲函數的 prop 在 React 組件間共享代碼的簡單技術。帶有render prop 的組件帶有一個返回一個React元素的函數並調用該函數而不是實現本身的渲染邏輯,並配上了這樣的示例:react
<DataProvider render={data => ( <h1>Hello {data.target}</h1> )}/> // DataProvider 內部的渲染邏輯相似於下面這樣 <div> <p>這是一行僞代碼</p> {this.props.render(data)} </div>
看到這裏,有可能你已經恍然大悟,原來所謂的Render Props設計模式不過如此,就是能夠用react組件的render屬性實現動態化的渲染邏輯。首先須要澄清兩點:git
不是利用react組件的render屬性,像咱們class組件擁有的render函數,而是給他自定義一個render函數,屬性名能夠叫child,也能夠叫what,能夠是除react固有屬性(key,className)之外的任何名字,好比用一個generate屬性傳遞渲染邏輯,而後渲染this.props.generate(data);github
<DataProvider render={data => (<h1>Hello {data.target}</h1>)} />
<DataProvider> {data => ( <h1>Hello {data.target}</h1> )} </DataProvider>
Render Props設計模式與咱們日常使用的通用組件(傳遞不一樣屬性,渲染不一樣結果)相比,後者只是常規的React組件編寫方式,用於同一個組件在不一樣的組件下調用,方便重用,擁有相同的渲染邏輯,更多用於展現型組件,而前者與高階組件(Hoc)同樣,是React組件的一種設計模式,用於方法的公用,渲染邏輯由調用者決定,更多用於容器型組件,固然強調點仍是方法的重用spring
在公司剛用react不久,我本身封裝了一款組件,是在antd的AutoComplete組件上進行封裝的,在個人一篇文章《antd組件使用進階及踩過的坑》提到過,動態效果以下:
而在具體實現裏我有這樣的一段代碼:npm
// 搜索過程代碼處理 this.setState({ loading: true, options: null }); // 獲取數據,並格式化數據 fetchData(params).then((list) => { let options; if (isEmpty(list)) { options = [DefaultOption]; } else { // **用戶自定義數據格式轉換;** options = format(list).map(({ label, value }, key) => ( <Option key={key} value={String(value)}>{label}</Option>)); } !options.length && options.push(DefaultOption); this.setState({ options, loading: false, seachRes: list }); }); // render實現部分代碼: <AutoComplete autoFocus className="certain-category-search" dropdownClassName="certain-category-search-dropdown" dropdownMatchSelectWidth size={size} onBlur={this.handleCloseSearch} onSearch={this.handleChange} onSelect={this.handleSelect} style={{ width: '100%' }} optionLabelProp="value" > {loading ? [<Option key="loading" disabled> <Spin spinning={loading} style={{ paddingLeft: '45%', textAlign: 'center' }} /> </Option>] : options } </AutoComplete> // 調用代碼 const originProps = { searchKey: 'keyword', fetchData: mockFetch, format: datas => datas.map(({ id, name }, index) => ({ label: `${name}(${id})`, value: name, key: index })) }; <OriginSearch {...originProps} />
當時本身對render props並不瞭解,如今分析一下代碼,會發現格式化數據的format屬性與render Props有一點像,只不過他調用的位置有點曲折(在遠程搜索函數中,根據搜索後的數據執行了渲染邏輯,而後更新到state中,在render函數中再從state中取出來渲染),從性能上來說也多了一丁點的消耗。這個format函數已經實現了咱們想要的,可是爲了對比,用children prop來實現了一下:segmentfault
// 設置loading狀態,清空option this.setState({ loading: true, options: null }); // 獲取數據 fetchData(params).then((list) => { if (fetchId !== this.lastFethId) { // 異步程序,保證回調是按順序進行 return; } this.setState({ loading: false, seachRes: list }); }); // render實現部分代碼: <AutoComplete autoFocus className="certain-category-search" dropdownClassName="certain-category-search-dropdown" dropdownMatchSelectWidth size={size} onBlur={this.handleCloseSearch} onSearch={this.handleChange} onSelect={this.handleSelect} style={{ width: '100%' }} optionLabelProp="value" > {loading ? [<Option key="loading" disabled> <Spin spinning={loading} style={{ paddingLeft: '45%', textAlign: 'center' }} /> </Option>] : this.props.children(seachRes) } </AutoComplete> // 調用代碼 const originProps = { searchKey: 'keyword', fetchData: mockFetch }; <OriginSearchWithRenderProp {...originProps}> {(datas) => datas.map(({id, name}, index) => ( <Option key={index}>{`${name}(${id})`}</Option> )) } </OriginSearchWithRenderProp>
能夠看到,當我用children prop來重寫這個組件時也是能夠的,並且內部邏輯看起來彷佛變得更簡單。可是不是就完美了,仍是值得推敲的,接着往下看。設計模式
上一節那一個示例的改寫,從render props定義去看,之前的寫法對於一個展現型的組件來講,其實更合適,調用者能夠編寫更少的邏輯,而改寫後對調用者就顯得有點麻煩,由於雖然調用者能夠本身定義渲染邏輯,可是AutoComplete這個組件能接收的子組件類型頗有限,對於我這個功能來講,Option是惟一能夠接收的子組件類型,因此意義不大。而render props這種模式定義的組件更着重於方法的重用,以及渲染邏輯能夠由調用者自定義。來看一看下面這一種需求:antd
這個需求是我司的一個優惠券運營定製需求,產品想實現各類動態(可增減,數目不定)規則的定義。這種表單是最讓人頭疼的,不過寫表單原本就是一件很讓人頭疼的事,最近看了篇文章《不再想寫表單了》圖文並茂,讓我深有感觸。我在上一篇文章《React文檔,除了主要概念,高級指引其實更好用》總結了怎樣用配置的寫antd表單。
回到正題,這個需求大體來看,第一: 須要自定義表單,經常使用的表單輸入,爲一個下拉框或一個輸入框什麼的,這裏都是一個字段有多個輸入,或則有多個選擇。第二: 每一個表單項要實現動態增減。因此若是不用設計模式的話,可能這四個表單項,你須要寫4個自定義的組件,況且我那個需求這種表單有8個。但若是仔細觀察,能夠發現,這4個表單有着類似的行爲,就是動態增減,每一個表單每一項數據結構類似,只是四個表單的渲染不一樣。因此這就是最適合render props這種設計模式來定義這個容器,被這個容器包裹的組件他們擁有一個增長和一個減小的方法,而後他們類似的數據結構大概是這樣:數據結構
const datas = [{ key: 0, value1: '', value2: '', }, { key: 1, value1: '', value2: '', }]
基於以上的總結咱們能夠這樣實現組件(看代碼):
export default class DaynamicForm extends React.Component { constructor(props) { super(props); const { value = [{ key: 0 }] } = props; this.state = { rules: value.map((ele, key) => ({ ...ele, key })), }; this.handlMinus = this.handlMinus.bind(this); this.handlAdd = this.handlAdd.bind(this); this.handleChange = this.handleChange.bind(this); } // 處理減項,邏輯刪除 handlMinus(index) { const { rules } = this.state; rules[index].deleteFlag = true; this.setState({ rules: [...rules] }); this.trigger(rules); } // 處理增項 handlAdd() { let { rules } = this.state; rules = rules.concat([{ value: undefined, key: rules.length }]); this.setState({ rules: [...rules] }); this.trigger(rules); } // 處理表單值的變化 handleChange(val, index, key) { const { rules } = this.state; rules[index][key] = val; this.setState({ rules: [...rules] }); this.trigger(rules); } // 觸發外部訂閱 trigger(res) { const { onChange } = this.props; onChange && onChange(res.filter(e => !e.deleteFlag)); } render() { const { children } = this.props; const { rules } = this.state; const actions = { handlAdd: this.handlAdd, handlMinus: this.handlMinus, handleChange: this.handleChange, }; return ( <div> {rules.filter(rule => !rule.deleteFlag).map(rule => ( <div key={rule.key}> {children(rule, actions)} {rule.key === 0 ? <Button style={{ marginLeft: 10 }} onClick={this.handlAdd} type="primary" shape="circle" icon="plus" /> : <Button style={{ marginLeft: 10 }} type="primary" shape="circle" icon="minus" onClick={() => this.handlMinus(rule.key)} /> } </div>) )} </div> ); } }
而調用的代碼,以滿減規則爲例:
<FormItem {...formItemLayout} label="滿減規則"> <DaynamicForm key="fullRules"> {(rule, actions) => <span key={rule.key}> <span>滿</span> <InputNumber style={{ margin: '0 5px', width: 100 }} value={rule.full} min={0.01} step={0.01} precision={2} placeholder=">0, 2位小數" onChange={e => actions.handleChange(e, rule.key, 'full')} />元,減 <InputNumber style={{ margin: '0 5px', width: 100 }} value={rule.reduction} min={0.01} step={0.01} precision={2} placeholder=">0, 2位小數" onChange={e => actions.handleChange(e, rule.key, 'reduction')} /> 元 </span>} </DaynamicForm> </FormItem>
從上面的組件實現代碼和調用代碼能夠看出,render props很好的實現了這一切,並很好的詮釋了方法公用,渲染邏輯自定義的概念。詳細代碼可參見示例項目
render props這種設計模式,我在使用兩種庫時見過:react-motion與apollo-graphql。react-motion是一個react動畫庫, 經常使用於個性化動畫的定義,好比實現一個平移動畫,先看效果:
實現代碼:
<Motion style={{x: spring(this.state.open ? 400 : 0)}}> {({x}) => // children is a callback which should accept the current value of // `style` <div className="demo0"> <div className="demo0-block" style={{ WebkitTransform: `translate3d(${x}px, 0, 0)`, transform: `translate3d(${x}px, 0, 0)`, }} /> </div> } </Motion>
apollo-graphql同時兼容了高階組件和render props的寫法,render props模式調用時以下面所示:
<Query query={GET_DOGS}> {({ loading, error, data }) => { if (loading) return "Loading..."; if (error) return `Error! ${error.message}`; return ( <select name="dog" onChange={onDogSelected}> {data.dogs.map(dog => ( <option key={dog.id} value={dog.breed}> {dog.breed} </option> ))} </select> ); }} </Query>
以上,就是Render Props,但願你看完能在本身的開發中用起來,若是你發現本文有什麼錯誤或不足之處,歡迎指正。