Implement a breadcrumb navigation using React high-order components

What are React higher-order components

React high-order components wrap the React components that need to be modified in the form of high-order functions and return the processed React components. React high-order components are used very frequently in the React ecosystem. For example, many APIs react-routerin withRouterand inreact-redux are implemented in this way.connect

<!– more –>

Benefits of using React higher-order components

At 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. However, the maintainability of the page will become extremely poor, and each page needs to be updated. Make changes to the same components in a page. Therefore, we can extract the common parts, such as accepting the same query operation results, wrapping the same label outside the component, etc., make a separate function, and pass in different business components as sub-component parameters, and this function It will not modify the sub-component, but wrap the sub-component in the container component through combination. It is a pure function without side effects, so that we can decouple this part of the code without changing the logic of these components, and improve the code Maintainability.

Implement a high-level component yourself

In front-end projects, breadcrumb navigation with link pointing is very commonly used, but because breadcrumb navigation needs to manually maintain an array of mappings of all directory paths and directory names, and we can react-routerget all the data here from the routing table, so we You can start here to implement a high-order component for breadcrumb navigation.

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

// Shown here is the route example of react-router4 
let routes = [
  {
    breadcrumb : 'First-level 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 : 'Third-level directory 1' ,
             path : '/a/b/c1' ,
             component : require ( '../a/b/c1/index.js' ). default ,
             exact : true ,
          },
          {
            breadcrumb : 'Third-level directory 2' ,
             path : '/a/b/c2' ,
             component : require ( '../a/b/c2/index.js' ). default ,
             exact : true ,
          },
      }
    ]
  }
]

// The ideal breadcrumbs component 
// Display format is a / b / c1 and attached 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 > 
);

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

The first one can be wrapped by the withRouter high-order component provided by react-router, which allows the sub-component to obtain the location attribute of the current page and thereby obtain the page path.

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

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

Then put the flattened directory path mapping and the current page path into the processing function to generate a breadcrumb navigation structure.

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

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

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

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

Then for each breadcrumb path part, generate the directory name and attach 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 (
         ' Each route in Router must contain `path` and `breadcrumb` attributes'
      );
    }
    // Find if there is a match 
    // exact is an attribute of react router4, used to accurately match routes 
    return  matchPath (pathSection, { path, exact : true });
  });

  // Return the breadcrumb value, if not, return the original matching sub-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 name of the root directory is the 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 pointed by the breadcrumb pathand the content mapping of the breadcrumb content. These two 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 a React higher-order component that can pass the current path and routing properties to the wrapped component. Pass in a component and return a new and identical component structure, so that it will not damage any functions and 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 ;

The method of calling this high-order component is also very simple. You only need to pass in the current path and the entire react routergenerated routesattribute.
As for how to obtain the current path, we can use the function react routerprovided withRouter. Please consult the relevant documentation on how to use it.
It is worth mentioning that withRouterit is a high-order component in itself and can provide locationseveral routing attributes including attributes for the wrapped component. Therefore, this API can also be used as a good reference for learning high-order components.

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

4. Q&A

  1. If react routerthe generated one routesis not manually maintained by yourself, it does not even exist locally, but is pulled through a request and stored in redux. When wrapped by the high-order function react-reduxprovided by, the route will not cause the breadcrumbs to change. connectComponent updates. How to use it:
function  mapStateToProps ( state ) {
   return {
     routes : state. routes ,
  };
}

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

This is actually a bugconnect in the function . Because the connect high-order component of react-redux will implement the shouldComponentUpdate hook function for the incoming parameter component, the update-related life cycle function (including render) will only be triggered when the prop changes . Obviously, our location object does not This parameter component is not passed in as prop.

The officially recommended approach is to use to withRouterwrap , that isconnectreturn value

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, will not cause any changes to the component type. Therefore, high-order components are like chain calls and can be wrapped in any number of layers to pass in different components to the component. The attributes can be changed at will under normal circumstances, making it very flexible in use. This pluggable feature makes high-end components very popular in the React ecosystem. You can see the shadow of this feature in many open source libraries, and you can take it out and analyze it when you have time.