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.
Comments