React组件Render Props VS HOC 设计模式

React的设计模式有很多种,比如无状态组件/表现型组件,有状态组件/容器型组件,render模式组件,高阶组件等等。本文主要介绍react的render模式与HOC设计模式,并通过实际案例进行比较。

render props模式

The Render Props是一种在不重复代码的情况下共享组件间功能的方法。如下所示:

<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>

通过使用prop来定义呈现的内容,组件只是注入功能,而不需要知道它如何应用于UI。render prop 模式意味着用户通过定义单独组件来传递prop方法,来指示共享组件应该返回的内容。

Render Props 的核心思想是,通过一个函数将class组件的state作为props传递给纯函数组件

import React from 'react';

const SharedComponent extends React.Component {
  state = {...}
  render() {
    return (
      <div>
        {this.props.render(this.state)}
      </div>
    );
  }
}

export default SharedComponent;

this.props.render()是由另外一个组件传递过来的。为了使用以上组件,我们可以进行下面的操作:

import React from 'react';
import SharedComponent from 'components/SharedComponent';

const SayHello = () => (
  <SharedComponent render={(state) => (
    <span>hello!,{...state}</span>
  )} />
);

{this.props.render(this.state)}这个函数,将其state作为参数传入其的props.render方法中,调用时直接取组件所需要的state即可。
render props模式最重要的是它返回一个react元素,比如我将上面的render属性改名,依然有效。

import React from 'react';

const SharedComponentWithGoofyName extends React.Component {
  render() {
    return (
      <div>
        {this.props.wrapThisThingInADiv()}
      </div>
    );
  }
}

const SayHelloWithGoofyName = () => (
  <SharedComponentWithGoofyName wrapThisThingInADiv={() => (
    <span>hello!</span>
  )} />
);

HOC设计模式

React的高阶组件主要用于组件之间共享通用功能而不重复代码的模式(也就是达到DRY模式)。

高阶组件实际是一个函数。 HOC函数将组件作为参数并返回一个新的组件。它将组件转换为另一个组件并添加额外的数据或功能。

高阶组件在React生态链技术中经常用到,对读者较为熟悉的,比如Redux中的connect,React Router中的withRouter等。

常见的高阶组件如下所示:

import React from 'react';

const withSecretToLife = (WrappedComponent) => {
  class HOC extends React.Component {
    render() {
      return (
        <WrappedComponent
          secretToLife={42}
          {...this.props}
        />
      );
    }
  }
    
  return HOC;
};

export default withSecretToLife;

已知secretToLife为42,有一些组件需要共享这个信息,此时创建了SecretToLife的HOC,将它作为prop传递给我们的组件。

import React from 'react';
import withSecretToLife from 'components/withSecretToLife';

const DisplayTheSecret = props => (
  <div>
    The secret to life is {props.secretToLife}.
  </div>
);

const WrappedComponent = withSecretToLife(DisplayTheSecret);

export default WrappedComponent;

此时,WrappedComponent只是DisplayTheSecret的增强版本,允许我们访问secretToLife属性。

Render Props与HOC模式实例对比

本文以一个利用localStorage API的小例子分别使用HOC设计模式跟The Render Props设计模式编写demo。

HOC Example

import React from 'react';

const withStorage = (WrappedComponent) => {
  class HOC extends React.Component {
    state = {
      localStorageAvailable: false, 
    };
  
    componentDidMount() {
       this.checkLocalStorageExists();
    }
  
    checkLocalStorageExists() {
      const testKey = 'test';

      try {
          localStorage.setItem(testKey, testKey);
          localStorage.removeItem(testKey);
          this.setState({ localStorageAvailable: true });
      } catch(e) {
          this.setState({ localStorageAvailable: false });
      } 
    }
  
    load = (key) => {
      if (this.state.localStorageAvailable) {
        return localStorage.getItem(key); 
      }
      
      return null;
    }
    
    save = (key, data) => {
      if (this.state.localStorageAvailable) {
        localStorage.setItem(key, data);
      }
    }
    
    remove = (key) => {
      if (this.state.localStorageAvailable) {
        localStorage.removeItem(key);
      }
    }
    
    render() {
      return (
        <WrappedComponent
          load={this.load}
          save={this.save}
          remove={this.remove}
          {...this.props}
        />
      );
    }
  }
    
  return HOC; 
}

export default withStorage;

在withStorage中,使用componentDidMount生命周期函数来检查checkLocalStorageExists函数中是否存在localStorage。

local,save,remove则是来操作localStorage的。现在我们创建一个新的组件,将其包裹在HOC组件中,用于显示相关的信息。由于获取信息的API调用需要很长时间,我们可以假设这些值一旦设定就不会改变。我们只会在未保存值的情况下进行此API调用。 然后,每当用户返回页面时,他们都可以立即访问数据,而不是等待我们的API返回。

import React from 'react';
import withStorage from 'components/withStorage';

class ComponentNeedingStorage extends React.Component {
  state = {
    username: '',
    favoriteMovie: '',
  }

  componentDidMount() {
    const username = this.props.load('username');
    const favoriteMovie = this.props.load('favoriteMovie');
    
    if (!username || !favoriteMovie) {
      // This will come from the parent component
      // and would be passed when we spread props {...this.props}
      this.props.reallyLongApiCall()
        .then((user) => {
          this.props.save('username', user.username) || '';
          this.props.save('favoriteMovie', user.favoriteMovie) || '';
          this.setState({
            username: user.username,
            favoriteMovie: user.favoriteMovie,
          });
        }); 
    } else {
      this.setState({ username, favoriteMovie })
    }
  }

  render() {
    const { username, favoriteMovie } = this.state;
    
    if (!username || !favoriteMovie) {
      return <div>Loading...</div>; 
    }
    
    return (
      <div>
        My username is {username}, and I love to watch {favoriteMovie}.
      </div>
    )
  }
}

const WrappedComponent = withStorage(ComponentNeedingStorage);

export default WrappedComponent;

在封装组件的componentDidMount内部,首先尝试从localStorage中获取,如果不存在,则异步调用,将获得的信息存储到localStorage并显示出来。

The Render Props Exapmle

import React from 'react';

class Storage extends React.Component {
    state = {
      localStorageAvailable: false, 
    };
  
    componentDidMount() {
       this.checkLocalStorageExists();
    }
  
    checkLocalStorageExists() {
      const testKey = 'test';

      try {
          localStorage.setItem(testKey, testKey);
          localStorage.removeItem(testKey);
          this.setState({ localStorageAvailable: true });
      } catch(e) {
          this.setState({ localStorageAvailable: false });
      } 
    }
  
    load = (key) => {
      if (this.state.localStorageAvailable) {
        return localStorage.getItem(key); 
      }
      
      return null;
    }
    
    save = (key, data) => {
      if (this.state.localStorageAvailable) {
        localStorage.setItem(key, data);
      }
    }
    
    remove = (key) => {
      if (this.state.localStorageAvailable) {
        localStorage.removeItem(key);
      }
    }
    
    render() {
      return (
        <span>
          this.props.render({
            load: this.load,
            save: this.save,
            remove: this.remove,
          })
        </span>
      );
    } 
}

Storage组件内部与HOC的withStorage较为类似,不同的是Storage不接受组件为参数,并且返回this.props.render。

import React from 'react';
import Storage from 'components/Storage';

class ComponentNeedingStorage extends React.Component {
  state = {
    username: '',
    favoriteMovie: '',
    isFetching: false,
  }

  fetchData = (save) => {
    this.setState({ isFetching: true });
    
    this.props.reallyLongApiCall()
      .then((user) => {
        save('username', user.username);
        save('favoriteMovie', user.favoriteMovie);

        this.setState({
          username: user.username,
          favoriteMovie: user.favoriteMovie,
          isFetching: false,
        });
      }); 
  }

  render() {
    return (
      <Storage
        render={({ load, save, remove }) => {
          const username = load('username') || this.state.username;
          const favoriteMovie = load('favoriteMovie') || this.state.username;
      
          if (!username || !favoriteMovie) {
            if (!this.state.isFetching) {
              this.fetchData(save);               
            }

            return <div>Loading...</div>; 
          }
      
          return (
            <div>
              My username is {username}, and I love to watch {favoriteMovie}.
            </div>
          );
        }}
      />
    )
  }
}

对于ComponentNeedingStorage组件来说,利用了Storage组件的render属性传递的三个方法,进行一系列的数据操作,从而展示相关的信息。

render props VS HOC模式

总的来说,render props其实和高阶组件类似,就是在puru component上增加state,响应react的生命周期。
对于HOC模式来说,优点如下:

  • 支持ES6
  • 复用性强,HOC为纯函数且返回值为组件,可以多层嵌套
  • 支持传入多个参数,增强了适用范围

当然也存在如下缺点:

  • 当多个HOC一起使用时,无法直接判断子组件的props是哪个HOC负责传递的
  • 多个组件嵌套,容易产生同样名称的props
  • HOC可能会产生许多无用的组件,加深了组件的层级

Render Props模式的出现主要是为了解决HOC所出现的问题。优点如下所示:

  • 支持ES6
  • 不用担心props命名问题,在render函数中只取需要的state
  • 不会产生无用的组件加深层级
  • render props模式的构建都是动态的,所有的改变都在render中触发,可以更好的利用组件内的生命周期。

当然笔者认为,对于Render Props与HOC两者的选择,应该根据不同的场景进行选择。Render Props模式比HOC更直观也更利于调试,而HOC可传入多个参数,能减少不少的代码量。

Render Props对于只读操作非常适用,如跟踪屏幕上的滚动位置或鼠标位置。 HOC倾向于更好地执行更复杂的操作,例如以上的localStorage功能。

参考文献

Understanding React Render Props by Example

Understanding React Higher-Order Components by Example

Ultimate React Component Patterns with Typescript 2.8

React Component Patterns

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容