什么是 React 高阶组件

React 高阶组件就是以高阶函数的形式包裹须要润饰的 React 组件,并返回解决实现后的 React 组件。React 高阶组件在 React 生态中应用的十分频繁,比方react-router 中的 withRouter 以及 react-reduxconnect 等许多 API 都是以这样的形式来实现的。

<!-- more -->

应用 React 高阶组件的益处

在工作中,咱们常常会有很多性能类似,组件代码反复的页面需要,通常咱们能够通过齐全复制一遍代码的形式实现性能,然而这样页面的保护可维护性就会变得极差,须要对每一个页面里的雷同组件去做更改。因而,咱们能够将其中独特的局部,比方承受雷同的查问操作后果、组件外同一的标签包裹等抽离进去,做一个独自的函数,并传入不同的业务组件作为子组件参数,而这个函数不会批改子组件,只是通过组合的形式将子组件包装在容器组件中,是一个无副作用的纯函数,从而咱们可能在不扭转这些组件逻辑的状况下将这部分代码解耦,晋升代码可维护性。

本人入手实现一个高阶组件

前端我的项目里,带链接指向的面包屑导航非常罕用,但因为面包屑导航须要手动保护一个所有目录门路与目录名映射的数组,而这里所有的数据咱们都能从 react-router 的路由表中获得,因而咱们能够从这里动手,实现一个面包屑导航的高阶组件。

首先咱们看看咱们的路由表提供的数据以及指标面包屑组件所须要的数据:

// 这里展现的是 react-router4 的route示例let routes = [  {    breadcrumb: '一级目录',    path: '/a',    component: require('../a/index.js').default,    items: [      {        breadcrumb: '二级目录',        path: '/a/b',        component: require('../a/b/index.js').default,        items: [          {            breadcrumb: '三级目录1',            path: '/a/b/c1',            component: require('../a/b/c1/index.js').default,            exact: true,          },          {            breadcrumb: '三级目录2',            path: '/a/b/c2',            component: require('../a/b/c2/index.js').default,            exact: true,          },      }    ]  }]// 现实中的面包屑组件// 展现格局为 a / b / c1 并都附上链接const BreadcrumbsComponent = ({ breadcrumbs }) => (  <div>    {breadcrumbs.map((breadcrumb, index) => (      <span key={breadcrumb.props.path}>        <link to={breadcrumb.props.path}>{breadcrumb}</link>        {index < breadcrumbs.length - 1 && <i> / </i>}      </span>    ))}  </div>);

这里咱们能够看到,面包屑组件须要提供的数据一共有三种,一种是以后页面的门路,一种是面包屑所带的文字,一种是该面包屑的导航链接指向。

其中第一种咱们能够通过 react-router 提供的 withRouter 高阶组件包裹,可使子组件获取到以后页面的 location 属性,从而获取页面门路。

后两种须要咱们对 routes 进行操作,首先将 routes 提供的数据扁平化成面包屑导航须要的格局,咱们能够应用一个函数来实现它。

/** * 以递归的形式展平react router数组 */const flattenRoutes = arr =>  arr.reduce(function(prev, item) {    prev.push(item);    return prev.concat(      Array.isArray(item.items) ? flattenRoutes(item.items) : item    );  }, []);

之后将展平的目录门路映射与以后页面门路一起放入处理函数,生成面包屑导航构造。

export const getBreadcrumbs = ({ flattenRoutes, location }) => {  // 初始化匹配数组match  let matches = [];  location.pathname    // 获得路径名,而后将门路宰割成每一路由局部.    .split('?')[0]    .split('/')    // 对每一部分执行一次调用`getBreadcrumb()`的reduce.    .reduce((prev, curSection) => {      // 将最初一个路由局部与以后局部合并,比方当门路为 `/x/xx/xxx` 时,pathSection别离查看 `/x` `/x/xx` `/x/xx/xxx` 的匹配,并别离生成面包屑      const pathSection = `${prev}/${curSection}`;      const breadcrumb = getBreadcrumb({        flattenRoutes,        curSection,        pathSection,      });      // 将面包屑导入到matches数组中      matches.push(breadcrumb);      // 传递给下一次reduce的门路局部      return pathSection;    });  return matches;};

而后对于每一个面包屑门路局部,生成目录名称并附上指向对应路由地位的链接属性。

const getBreadcrumb = ({ flattenRoutes, curSection, pathSection }) => {  const matchRoute = flattenRoutes.find(ele => {    const { breadcrumb, path } = ele;    if (!breadcrumb || !path) {      throw new Error(        'Router中的每一个route必须蕴含 `path` 以及 `breadcrumb` 属性'      );    }    // 查找是否有匹配    // exact 为 react router4 的属性,用于准确匹配路由    return matchPath(pathSection, { path, exact: true });  });  // 返回breadcrumb的值,没有就返回原匹配子路径名  if (matchRoute) {    return render({      content: matchRoute.breadcrumb || curSection,      path: matchRoute.path,    });  }  // 对于routes表中不存在的门路  // 根目录默认名称为首页.  return render({    content: pathSection === '/' ? '首页' : curSection,    path: pathSection,  });};

之后由 render 函数生成最初的单个面包屑导航款式。单个面包屑组件须要为 render 函数提供该面包屑指向的门路 path, 以及该面包屑内容映射content 这两个 props。

/** * */const render = ({ content, path }) => {  const componentProps = { path };  if (typeof content === 'function') {    return <content {...componentProps} />;  }  return <span {...componentProps}>{content}</span>;};

有了这些性能函数,咱们就能实现一个能为包裹组件传入以后所在门路以及路由属性的 React 高阶组件了。传入一个组件,返回一个新的雷同的组件构造,这样便不会对组件外的任何性能与操作造成毁坏。

const BreadcrumbsHoc = (  location = window.location,  routes = []) => Component => {  const BreadComponent = (    <Component      breadcrumbs={getBreadcrumbs({        flattenRoutes: flattenRoutes(routes),        location,      })}    />  );  return BreadComponent;};export default BreadcrumbsHoc;

调用这个高阶组件的办法也非常简单,只须要传入以后所在门路以及整个 react router 生成的 routes 属性即可。
至于如何获得以后所在门路,咱们能够利用 react router 提供的 withRouter 函数,如何应用请自行查阅相干文档。
值得一提的是,withRouter 自身就是一个高阶组件,能为包裹组件提供包含 location 属性在内的若干路由属性。所以这个 API 也能作为学习高阶组件一个很好的参考。

withRouter(({ location }) =>  BreadcrumbsHoc(location, routes)(BreadcrumbsComponent));

Q&A

如果react router 生成的 routes 不是由本人手动保护的,甚至都没有存在本地,而是通过申请拉取到的,存储在 redux 里,通过 react-redux 提供的 connect 高阶函数包裹时,路由发生变化时并不会导致该面包屑组件更新。应用办法如下:

function mapStateToProps(state) {  return {    routes: state.routes,  };}connect(mapStateToProps)(  withRouter(({ location }) =>    BreadcrumbsHoc(location, routes)(BreadcrumbsComponent)  ));

这其实是 connect 函数的一个bug。因为 react-redux 的 connect 高阶组件会为传入的参数组件实现 shouldComponentUpdate 这个钩子函数,导致只有 prop 发生变化时才触发更新相干的生命周期函数(含 render),而很显然,咱们的 location 对象并没有作为 prop 传入该参数组件。

官网举荐的做法是应用 withRouter 来包裹 connectreturn value,即

withRouter(  connect(mapStateToProps)(({ location, routes }) =>    BreadcrumbsHoc(location, routes)(BreadcrumbsComponent)  ));

其实咱们从这里也能够看出,高阶组件同高阶函数一样,不会对组件的类型造成任何更改,因而高阶组件就如同链式调用一样,能够任意多层包裹来给组件传入不同的属性,在失常状况下也能够随便调换地位,在应用上十分的灵便。这种可插拔个性使得高阶组件十分受 React 生态的青眼,很多开源库里都能看到这种个性的影子,有空也能够都拿进去剖析一下。