An example of breadcrumb navigation using React high order components

  • 2021-08-06 20:58:53
  • OfStack

What are React high-order components

React high-order component is to wrap the React component to be modified in the way of high-order function, and return the React component after processing. React high-order components are frequently used in React ecosystem, such as withRouter in react-router and connect in react-redux, etc. Many API are implemented in this way.

Benefits of using React high-order components

In our work, we often have many page requirements with similar functions and repeated component codes. Usually, we can realize the functions by completely copying the code once, but the maintainability of the page will become extremely poor, and we need to make changes to the same components in every page. Therefore, we can combine the common parts, For example, accepting the same query operation results, pulling out the same 1 label package outside the component, etc. Do a single function, Different business components are passed in as sub-component parameters, but this function does not modify sub-components, but wraps sub-components in container components by combination, which is a pure function without side effects, so that we can decouple this part of code without changing the logic of these components and improve the maintainability of code.

Implement a high-order component by yourself

In front-end projects, breadcrumb navigation with link points is commonly used. However, breadcrumb navigation needs to manually maintain an array of all directory paths and directory names, and all the data here can be obtained from the routing table of react-router, so we can start from here and realize a high-order component of breadcrumb navigation.

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


//  What is shown here is  react-router4  Adj. route Example 
let routes = [
 {
  breadcrumb: '1 Level directory ',
  path: '/a',
  component: require('../a/index.js').default,
  items: [
   {
    breadcrumb: '2 Level directory ',
    path: '/a/b',
    component: require('../a/b/index.js').default,
    items: [
     {
      breadcrumb: '3 Level directory 1',
      path: '/a/b/c1',
      component: require('../a/b/c1/index.js').default,
      exact: true,
     },
     {
      breadcrumb: '3 Level directory 2',
      path: '/a/b/c2',
      component: require('../a/b/c2/index.js').default,
      exact: true,
     },
   }
  ]
 }
]

//  Ideal breadcrumb assembly 
//  The display format is  a / b / c1  And attach links to them 
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>
);

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

Among them, the first one can be wrapped by withRouter high-level components provided by react-router, which enables sub-components to obtain location attributes to the current page, thus obtaining the page path.

The latter two require us to operate routes. First, flatten the data provided by routes into the format required for breadcrumb navigation. We can use a function to realize it.


/**
 *  Flattening in a recursive way 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
  );
 }, []);

After that, the flattened directory path map and the current page path 1 are put into the processing function to generate the breadcrumb navigation structure.


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

 location.pathname
  //  Gets the path name, and then divides the path into each 1 Routing section .
  .split('?')[0]
  .split('/')
  //  To each 1 Partial execution 1 Secondary call `getBreadcrumb()` Adj. reduce.
  .reduce((prev, curSection) => {
   //  Will last 1 The route part is merged with the current part, such as when the path is  `/x/xx/xxx`  When, pathSection Check separately  `/x` `/x/xx` `/x/xx/xxx`  Match, and generate breadcrumbs respectively 
   const pathSection = `${prev}/${curSection}`;
   const breadcrumb = getBreadcrumb({
    flattenRoutes,
    curSection,
    pathSection,
   });

   //  Import breadcrumbs into matches Array 
   matches.push(breadcrumb);

   //  Pass to the following 1 Times reduce The path part of 
   return pathSection;
  });
 return matches;
};

Then, for every 1 breadcrumb path part, a directory name is generated with a link attribute to the corresponding route location.


const getBreadcrumb = ({ flattenRoutes, curSection, pathSection }) => {
 const matchRoute = flattenRoutes.find(ele => {
  const { breadcrumb, path } = ele;
  if (!breadcrumb || !path) {
   throw new Error(
    'Router Every one in 1 A route Must contain  `path`  As well as  `breadcrumb`  Attribute '
   );
  }
  //  Find if there is a match 
  // exact  For  react router4  Is used to accurately match routes 
  return matchPath(pathSection, { path, exact: true });
 });

 //  Return breadcrumb If there is no, the original matching subpathname is returned 
 if (matchRoute) {
  return render({
   content: matchRoute.breadcrumb || curSection,
   path: matchRoute.path,
  });
 }

 //  For routes Path that does not exist in the table 
 //  The default name of the root directory is Home Page .
 return render({
  content: pathSection === '/' ? ' Home page ' : curSection,
  path: pathSection,
 });
};

The final single breadcrumb navigation style is then generated by the render function. A single breadcrumb component needs to provide the render function with the path path to which the breadcrumb points and the breadcrumb content mapping content both props.


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

With these functions, we can implement an React high-level component that can pass in the current path and routing attributes for the package component. Pass in 1 component and return 1 new identical component structure so that no damage is done to any function or operation outside the component.


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

The method for calling this high-level component is also very simple, just passing in the current path and the routes properties generated by the entire react router.
As for how to get the current path, we can use the withRouter function provided by react router. Please refer to relevant documents for how to use it.
It is worth mentioning that withRouter itself is a high-level component, which can provide several routing attributes including location attributes for wrapping components. Therefore, this API can also be used as a good reference for learning high-order components.


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

Q & A

If the routes generated by react router is not manually maintained by itself, or even exists locally, but is pulled by request, stored in redux, and wrapped by connect high-order function provided by react-redux, the breadcrumb component will not be updated when the route changes. The usage method is as follows:


function mapStateToProps(state) {
 return {
  routes: state.routes,
 };
}

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

This is actually an bug of the connect function. Because the connect high-order component of react-redux will implement the hook function shouldComponentUpdate for the passed-in parameter component, the update of the related life cycle function (including render) will only be triggered when prop changes, and obviously, our location object is not passed into this parameter component as prop.

The officially recommended practice is to wrap return value of connect with withRouter, that is


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

In fact, we can also see from here that high-order components, like high-order functions 1, will not cause any changes to the type of components. Therefore, high-order components are just like chain calls 1, which can be wrapped in multiple layers to pass different attributes to components, and can change positions at will under normal circumstances, which is very flexible in use. This pluggable feature makes high-level components very popular with React ecology. Many open source libraries can see the shadow of this feature, and they can be analyzed when they are free.


Related articles: