A breadcrumb navigation system based on react

Time:2021-6-11

What is react high level component

React high-order component is to wrap the react component that needs to be modified in the form of high-order function, and return the react component after processing. React high-level components are frequently used in react ecosystem, such asreact-routerInwithRouteras well asreact-reduxinconnectMany APIs are implemented in this way.

<!– more –>

Benefits of using react advanced components

In our work, we often have a lot of page requirements with similar functions and duplicate component code. Usually, we can achieve the function by completely copying the code, but the maintainability of the page will become extremely poor, and we need to make changes to the same components in each page. Therefore, we can separate the common parts, such as accepting the same query operation results and the same label package outside the component, make a separate function, and pass in different business components as sub component parameters. This function does not modify the sub components, but only packages the sub components in the container component by combination, It is a pure function without side effects, so we can decouple this part of the code without changing the logic of these components, and improve the maintainability of the code.

Do it yourself to implement a high-level component

In front-end projects, breadcrumb navigation with links is very common, but breadcrumb navigation needs to manually maintain an array of all directory paths and directory names, and all the data here can be downloaded fromreact-routerSo we can start here and implement a high-level 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

//Here is a 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: '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,
          },
      }
    ]
  }
]

//Ideal crumb components
//The presentation format is a / B / C1 with links attached
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 that the breadcrumb component needs to provide, one is the path of the current page, the other is the text of the breadcrumb, and the other is the navigation link of the breadcrumb.

For the first one, we can use the withrouter high-level component package provided by react router to make the sub component obtain the location attribute of the current page, so as to obtain the page path.

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

/**
 *Flattening the 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
    );
  }, []);

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

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

  location.pathname
    //Get the path name, and then divide the path into each routing part
    .split('?')[0]
    .split('/')
    //Call 'getbreadcrumb()' reduce once for each part
    .reduce((prev, curSection) => {
      //Merge the last routing part with the current part. For example, when the path is' / X / XX / xxx ', pathsection checks the matching of' / X '/ X / XX' / X / XX / xxx 'and generates breadcrumbs respectively
      const pathSection = `${prev}/${curSection}`;
      const breadcrumb = getBreadcrumb({
        flattenRoutes,
        curSection,
        pathSection,
      });

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

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

Then, for each breadcrumb path section, the directory name is generated and the link attribute pointing to the corresponding route location is attached.

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' path 'and' breadcrumb 'attributes'
      );
    }
    //Find out if there is a match
    //Exact is the property of react router4, which is used to accurately match the route
    return matchPath(pathSection, { path, exact: true });
  });

  //Return the value of breadcrumb, and return the original matching subpath name if not
  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: cursection,
    path: pathSection,
  });
};

After that, the render function generates the final single breadcrumb navigation style. A single breadcrumb component needs to provide the path that the breadcrumb points to for the render functionpath, and the breadcrumb content mappingcontentThese 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 high-level component that can pass in the current path and routing attributes for the package component. Pass in a component and return a new and same component structure, so that any function and operation outside the component will not be damaged.

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-level component is also very simple. You only need to pass in the current path and the entirereact routerGeneratedroutesProperty.
As for how to get the current path, we can make use of itreact routerProvidedwithRouterFunction, how to use, please refer to the relevant documents.
It is worth mentioning that,withRouterIt is a high-level component, which can provide package components withlocationSeveral routing attributes, including. So this API can also be used as a good reference for learning high-level components.

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

Q&A

Ifreact routerGeneratedroutesIt’s not maintained manually. It doesn’t even exist locally. Instead, it’s pulled by request and stored in reduxreact-reduxProvidedconnectWhen the higher-order function is wrapped, the bread crumb component will not be updated when the route changes. The usage is as follows:

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

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

This is actuallyconnectAn example of functionbug. Because the connect high-level component of react Redux will implement the hook function shouldcomponentupdate for the incoming parameter component, resulting inOnly when the prop changes can the related lifecycle functions (including render) be updatedObviously, our location object is not passed in as a prop.

The official recommendation is to usewithRouterHere’s the packageconnectOfreturn value, i.e

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

In fact, we can see from here that high-order components, like high-order functions, will not cause any changes to the type of components. Therefore, high-order components are just like chain calls. They can pass different attributes to components through arbitrary multi-layer packages. Under normal circumstances, they can also switch positions at will, which is very flexible in use. This pluggable feature makes high-level components very popular with react ecosystem. Many open source libraries can see the shadow of this feature, and you can analyze it when you have time.