React組件經常使用設計模式之Render Props

本身在總結最近半年的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>)}
    />
  • 上面這種把渲染邏輯寫在prop屬性中,若是子組件渲染代碼多時,看着機會讓人感受有點凌亂,因此更多的時候咱們是利用react組件固有的children屬性來實現這種設計模式,因此render props更多的時候被稱之爲使用children prop。
<DataProvider>
  {data => (
    <h1>Hello {data.target}</h1>
  )}
 </DataProvider>

Render Props設計模式與咱們日常使用的通用組件(傳遞不一樣屬性,渲染不一樣結果)相比,後者只是常規的React組件編寫方式,用於同一個組件在不一樣的組件下調用,方便重用,擁有相同的渲染邏輯,更多用於展現型組件,而前者與高階組件(Hoc)同樣,是React組件的一種設計模式,用於方法的公用,渲染邏輯由調用者決定,更多用於容器型組件,固然強調點仍是方法的重用spring

初識render props

在公司剛用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來重寫這個組件時也是能夠的,並且內部邏輯看起來彷佛變得更簡單。可是不是就完美了,仍是值得推敲的,接着往下看。設計模式

children prop正確打開方式

上一節那一個示例的改寫,從render props定義去看,之前的寫法對於一個展現型的組件來講,其實更合適,調用者能夠編寫更少的邏輯,而改寫後對調用者就顯得有點麻煩,由於雖然調用者能夠本身定義渲染邏輯,可是AutoComplete這個組件能接收的子組件類型頗有限,對於我這個功能來講,Option是惟一能夠接收的子組件類型,因此意義不大。而render props這種模式定義的組件更着重於方法的重用,以及渲染邏輯能夠由調用者自定義。來看一看下面這一種需求:antd

clipboard.png

這個需求是我司的一個優惠券運營定製需求,產品想實現各類動態(可增減,數目不定)規則的定義。這種表單是最讓人頭疼的,不過寫表單原本就是一件很讓人頭疼的事,最近看了篇文章《不再想寫表單了》圖文並茂,讓我深有感觸。我在上一篇文章《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,但願你看完能在本身的開發中用起來,若是你發現本文有什麼錯誤或不足之處,歡迎指正。

相關文章
相關標籤/搜索