What is the React high order component

The React high-order component wraps the React component as a high-order function and returns the React component after processing. React high-order components are used frequently in the React ecosystem. For example, withRouter in React – Router and Connect in React – Redux are implemented in this way.

The benefits of using React higher-order components

At work, we often have a lot of similar functions, component code repeated page requirements, usually we can completely copy the code to achieve the function, but such page maintenance and maintainability will become extremely poor, need to make changes to the same components in each page. So we can be one of the common parts, such as accept the same query operation results, components outside the same pulled out such as the label of the package, do a separate function, and introduced into different business components as a child component parameters, and the function does not modify the child components, only the child components by means of combination packing in container components, is a pure function has no side effects, This allows us to decouple this part of the code without changing the logic of these components, improving code maintainability.

Implement a high-level component yourself

Front end of projects, with a link to the bread crumbs navigation is very common, but because the breadcrumb navigation all need to manually maintain a directory path and directory name mapping array, where all the data we can from the react – the router’s routing table, so we can start from here, to implement a breadcrumb navigation higher-order components.

First let’s look at the data provided by our routing table and the data required by the target breadcrumb component:

// Here is an example of a route for react- Router4
let routes = [
  {
    breadcrumb: 'Primary directory'.path: '/a'.component: require('.. /a/index.js').default,
    items: [{breadcrumb: 'Secondary directory'.path: '/a/b'.component: require('.. /a/b/index.js').default,
        items: [{breadcrumb: 'Level 3 Directory 1'.path: '/a/b/c1'.component: require('.. /a/b/c1/index.js').default,
            exact: true}, {breadcrumb: 'Level 3 Directory 2'.path: '/a/b/c2'.component: require('.. /a/b/c2/index.js').default,
            exact: true,},}]}]// The ideal breadcrumb component
// Display in a/B/c1 format with links
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>
);
Copy the code

Here we can see that the breadcrumb component needs to provide three kinds of data, one is the path of the current page, one is the text of the breadcrumb, and one is the navigation link pointing to the breadcrumb.

First, we can use the High-order withRouter component package provided by the React-Router to enable the child component to obtain the location property of the current page, thus obtaining the page path.

The last two require us to operate routes. First, we flattish the data provided by routes into the format required for breadcrumb navigation. We can use a function to achieve this.

/** * recursively scales the React router array */
const flattenRoutes = arr= >
  arr.reduce(function(prev, item) {
    prev.push(item);
    return prev.concat(
      Array.isArray(item.items) ? flattenRoutes(item.items) : item ); } []);Copy the code

The flattened directory path map is then put into the processing function along with the current page path to generate the breadcrumb navigation structure.

export const getBreadcrumbs = ({ flattenRoutes, location }) = > {
  // Initializes the array match
  let matches = [];

  location.pathname
    // Get the pathname and split the path into each part of the route.
    .split('? ') [0]
    .split('/')
    // Perform a reduce call to 'getBreadcrumb()' for each part.
    .reduce((prev, curSection) = > {
      // Merge the last part of the route with the current part. For example, when the path is' /x/xx/ XXX ', the pathSection checks for the match of '/x' /x/xx '/x/xx/ XXX' and generates breadcrumbs respectively
      const pathSection = `${prev}/${curSection}`;
      const breadcrumb = getBreadcrumb({
        flattenRoutes,
        curSection,
        pathSection,
      });

      // Import the breadcrumbs into the matches array
      matches.push(breadcrumb);

      // The part of the path passed to the next reduce
      return pathSection;
    });
  return matches;
};
Copy the code

Then, for each breadcrumb path section, generate a directory name with a link attribute pointing to the corresponding routing location.

const getBreadcrumb = ({ flattenRoutes, curSection, pathSection }) = > {
  const matchRoute = flattenRoutes.find(ele= > {
    const { breadcrumb, path } = ele;
    if(! breadcrumb || ! path) {throw new Error(
        'Every route in the Router must contain a' path 'and a' breadcrumb 'attribute'.
      );
    }
    // Find if there is a match
    // Exact is the attribute of React Router4, which matches routes precisely
    return matchPath(pathSection, { path, exact: true });
  });

  // Returns the value of the breadcrumb, if not the original matching child path name
  if (matchRoute) {
    return render({
      content: matchRoute.breadcrumb || curSection,
      path: matchRoute.path,
    });
  }

  // For paths that do not exist in the routes table
  // The default root directory name is home.
  return render({
    content: pathSection === '/' ? 'home' : curSection,
    path: pathSection,
  });
};
Copy the code

The render function then generates the final individual breadcrumb navigation style. A single breadcrumb component needs to provide the Render function with props for the path to which the breadcrumb points, and the breadcrumb content mapping content.

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

With these functions, we can implement a High-order React component that passes in the current path and route properties for the package component. Passing in a component returns a new, identical component structure without breaking any functionality or operations outside the component.

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

Calling this higher-level component is also very simple, passing in the current path and the Routes attribute generated by the react Router. The React Router provides the withRouter function to obtain the current path. See the documentation for how to use this function. It is worth noting that the withRouter is itself a high-level component that provides several routing properties for the package component, including the Location property. So this API is also a good reference for learning about higher-order components.

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

4. Q&A

  1. ifreact routerThe generatedroutesIt’s not maintained manually by itself, it doesn’t even exist locally, it’s pulled by request, stored in Redux, throughreact-reduxTo provide theconnectWhen a higher-order function wraps, routing changes do not cause the breadcrumb component to update. The usage method is as follows:
function mapStateToProps(state) {
  return {
    routes: state.routes,
  };
}

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

This is actually a bug in the connect function. Because the React-Redux connect high-order component shouldComponentUpdate is implemented for the component that is passed in as a parameter, this should trigger the render function only when the prop changes. Our Location object is not passed as a prop to the parameter component.

The official recommendation is to use withRouter to wrap connect’s return value, i.e

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

In fact, we can also see from here, the high order component as higher-order functions, won’t cause any changes to the type of component, thus high-order component like chain calls, can be arbitrary multilayer package for components into different properties, under normal circumstances can also be repositioned at random, is very flexible in terms of use. This pluggable feature makes higher-order components popular in the React ecosystem, and it can be found in many open source libraries.