How to implement nested routing and dynamic routing in React Router?

May 6, 2023 1344hotness 0likes 0comments

React Router is a routing library for React applications that provides an easy way to match URL to components. React Router implements the following main concepts:

  • Router: it provides the basic routing capabilities of the application.
  • Routes: it defines the mapping between URL and components.
  • Link: it provides a convenient way to navigate through the application.
  • Switch: it is used to ensure that only one route can match the current URL.
  • createBrowserHistory: this is used to create an instance of HTML5 History API.

next, we will delve into the implementation principle of React Router. We will first discuss the implementation of Router components, then discuss the implementation of Routes components, and finally discuss the implementation of Link components.

implementation of Router components

The

Router component is the core component of the React Router library, which provides the basic routing capabilities of the application. The following is the simplified version of the Router component implementation code:

const Router = ({ children }) => {
  const [location, setLocation] = useState(window.location.pathname);

  useEffect(() => {
    const handlePopState = () => setLocation(window.location.pathname);
    window.addEventListener('popstate', handlePopState);
    return () => window.removeEventListener('popstate', handlePopState);
  }, []);

  return <RouterContext.Provider value={{ location }}>{children}</RouterContext.Provider>;
};

in the above code, we first define a Router component. It accepts a children attribute, which is the root component of our application. Then we use useState Hook to create a state variable named location . It will be used to track the current URL. We will update this state variable using the setLocation function.

next, we use useEffect Hook to register a function that listens for popstate events. The popstate event is triggered when the user clicks the browser's forward or back buttons. In this case, we update the location state variable to reflect the new URL.

Finally, we use the RouterContext.Provider component to pass the location state variable to its child components.

implementation of Routes components

The

Routes component is used to define the mapping between URL and components. The following is the simplified version of the Routes component implementation code:

const Routes = ({ children }) => {
  const { location } = useContext(RouterContext);

  return children.find((child) => matchPath(location, child.props)) || null;
};

in the above code, we first define a Routes component. It accepts a children attribute, which is a list of components that contain all routes for our application. Then, we use useContext Hook to get the location variable, which is passed from the Router component.

next, we use the find function to find the first route that matches the current URL in the children list. We use the matchPath function to compare the path attributes of the current URL and the route. If a matching route is found, the component corresponding to that route is returned. Otherwise, return null .

The

matchPath function is a function that compares the URL and route path attributes. The following is the simplified implementation code of the matchPath function:

const matchPath = (pathname, { path }) => {
  const segments = pathname.split('/').filter(Boolean);
  const parts = path.split('/').filter(Boolean);

  if (segments.length !== parts.length) return false;

  const params = {};

  for (let i = 0; i < parts.length; i++) {
    const isParam = parts[i].startsWith(':');
    if (isParam) {
      const paramName = parts[i].slice(1);
      const paramValue = segments[i];
      params[paramName] = paramValue;
    } else if (segments[i] !== parts[i]) {
      return false;
    }
  }

  return { params };
};

in the above code, we first define a matchPath function. It accepts two parameters: pathname is the path part of the current URL, and {path} is the path attribute of the routing component.

then we split the URL and route path attributes into segments. We use filter (Boolean) to filter out empty segments. Next, we compare whether the number of segments in URL is equal to the number of segments in the route. If they are not equal, then they cannot match, and we return false .

if they have the same number of segments, they may match. Next, we create an empty object params , which will be used to store the key-value pair of the URL parameter. Then, we iterate through each segment of the route, and if this segment is a parameter (that is, starting with a colon), the corresponding URL segment is stored in the params object. Otherwise, if this segment is not a parameter and is not equal to the corresponding segment of URL, then they cannot match, and we return false .

finally, if the URL and route can match, an object containing the URL parameter is returned. Otherwise, return false .

implementation of Link components

The

Link component is used to navigate through the application. The following is the simplified version of the Link component implementation code:

const Link = ({ to, ...rest }) => (
  <a href={to} onClick={(event) => {
    event.preventDefault();
    history.push(to);
  }} {...rest} />
);

in the above code, we first define a Link component. It accepts a to attribute, which is a string pointing to the URL we want to navigate to. Next, we use the preventDefault function to block the default link behavior and the history.push function to add URL to the history. Finally, we pass the other attributes passed to the Link component to the & lt;a> element through the spread operator.

implementation of Switch components

The

Switch component is a very important part of React Router to ensure that only one route matches the current URL. The following is the simplified version of the Switch component implementation code:

const Switch = ({ children }) => {
  const [match, setMatch] = useState(false);

  useEffect(() => {
    // iterate through all the child elements to find the first Route component that matches the current URL
    React.Children.forEach(children, (child) => {
      if (!match && React.isValidElement(child) && child.type === Route) {
        const { path, exact, strict, sensitive } = child.props;
        const match = matchPath(window.location.pathname, {
          path,
          exact,
          strict,
          sensitive,
        });
        if (match) {
          setMatch(true);
        }
      }
    });
  }, [children, match]);

  // return the first matching Route component
  return React.Children.toArray(children).find((child) => {
    return match && React.isValidElement(child) && child.type === Route;
  }) || null;
};

this Switch component is implemented in a very simple way. It uses useState and useEffect hooks to maintain a match state to indicate whether the current URL matches any of the child Route components. In the useEffect hook, it iterates through all the child elements, finds the first Route component that matches the current URL, and then sets the match state to true . In the return value, it iterates through all the child elements again, finds the first matching Route component, and returns it. If there is no matching Route component, return null .

The

Switch component ensures that only one route matches the current URL. The advantage of this is that you can avoid multiple routes matching the same URL at the same time, resulting in multiple components on the page. For example, in the following code, if there is no Switch component, both HomePage and AboutPage components will render:

<Route path="/" exact component={HomePage} />
<Route path="/about" component={AboutPage} />

and with the Switch component, only the first matching route will be rendered, so only the HomePage component will be rendered.

<Switch>
  <Route path="/" exact component={HomePage} />
  <Route path="/about" component={AboutPage} />
</Switch>

createBrowserHistory  function implementation

here is a simplified version of the createBrowserHistory function, which can be used to create a browser that supports HTML5 history API history object:

here, we introduce the history object. The history object is a JavaScript object that manages application history and can be used to navigate and listen for changes in URL. In React Router, the history object can be obtained by using useHistory Hook or by passing the history object to the component as props.

const createBrowserHistory = () => {
  let listeners = [];
  let location = {
    pathname: window.location.pathname,
    search: window.location.search,
    hash: window.location.hash,
  };

  const push = (pathname) => {
    window.history.pushState({}, '', pathname);
    location = { ...location, pathname };
    listeners.forEach(listener => listener(location));
  };

  window.addEventListener('popstate', () => {
    location = {
      pathname: window.location.pathname,
      search: window.location.search,
      hash: window.location.hash,
    };
    listeners.forEach(listener => listener(location));
  });

  return {
    get location() {
      return location;
    },
    push,
    listen(listener) {
      listeners.push(listener);
      return () => {
        listeners = listeners.filter(l => l !== listener);
      };
    },
  };
};

in the above code, we first define a createBrowserHistory function, which is used to create a browser history object that supports the HTML5 history API. This function returns an object with three methods: get location () , push (pathname) , and listen (listener) .

The

get location () method returns the current location object, which contains pathname , search and hash attributes, corresponding to the path part, query parameter, and hash part of the current URL, respectively.

The

push (pathname) method is used to add the specified pathname to the history and trigger all registered listeners.

The

listen (listener) method registers a location change listener and returns a function that unregisters the listener.

in React Router, we can use the createBrowserHistory function to create a browser history object and pass it as the history property of the Router component. In this way, we can use the same history object throughout the application to achieve unified URL management and navigation behavior.

the following is an implementation of a simplified version of the Router component, which uses the createBrowserHistory function to create a browser history object and passes it to the child component as the history attribute of the Router component:

const Router = ({ children }) => {
  const [location, setLocation] = useState(history.location);

  useEffect(() => {
    const unlisten = history.listen((newLocation) => {
      setLocation(newLocation);
    });

    return () => {
      unlisten();
    };
  }, []);

  return (
    <RouterContext.Provider value={{ location }}>
      {children}
    </RouterContext.Provider>
  );
};

const App = () => (
  <Router>
    <div>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Switch>
        <Route path="/about">
          <About />
        </Route>
        <Route path="/">
          <Home />
        </Route>
      </Switch>
    </div>
  </Router>
);

in the above code, we first define a Router component that accepts a children attribute that contains all the subcomponents. In the Router component, we use useState Hook to track the current location object and use useEffect Hook to register a history change listener. Whenever history changes, we can update the location state and pass it to all child components.

in the Router component, we also use a RouterContext context to pass the location state to the child components. We can access the location state by using useContext Hook in the subcomponents, thus realizing the function of rendering different components according to URL.

in the App component, we wrap all the subcomponents in the Router component and use the Link , Switch and Route components to define the navigation rules for the application. Whenever the user clicks on the Link component, we can use the history.push function to add the new URL to the history and trigger the location change listener registered in the Router component. Then, the Switch component matches the corresponding Route component based on the current code, and renders the matching component. In this way, we implement a simple routing system.

hopefully these code examples and annotations will help you understand how React Router works. Of course, this is just a simplified version of the implementation, and the actual React Router code is more complex, including many additional features and performance optimizations, such as dynamic routing, code segmentation, asynchronous loading, and so on. If you are interested in learning more about how React Router is implemented, it is recommended to read the official documentation and source code.

overall, React Router is a very powerful and flexible routing library that provides rich navigation and URL management capabilities for React applications, which can help us build complex single-page and multi-page applications.

InterServer Web Hosting and VPS

Aaron

Hello, my name is Aaron and I am a freelance front-end developer

Comments