本身在总结最近半年的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,但愿你看完能在本身的开发中用起来,若是你发现本文有什么错误或不足之处,欢迎指正。