Building a Simple Routing Solution for React
This article guides you through crafting a bespoke routing solution for React applications, prioritizing simplicity and maintainability. We'll be leveraging TypeScript for enhanced type safety and code clarity, aiming to replace React Router in smaller applications without the complexity of a heavyweight solution.
Step 1. Setup Navigation Context and Provider
The core of our routing system lies within the `navigation.tsx` file. Here, we define the NavigationContext which encapsulates the current path (`currentPath`) and navigation function (`navigate`).
navigation.tsx
import React, { createContext, useEffect, useState } from "react";
type NavigationContextType = {
currentPath: string;
navigate: (to: string) => void;
};
const NavigationContext = createContext<NavigationContextType>(
{} as NavigationContextType
);
const NavigationProvider = ({ children }: { children: React.ReactNode }) => {
const [currentPath, setCurrentPath] = useState(window.location.pathname);
useEffect(() => {
const handler = () => {
setCurrentPath(window.location.pathname);
};
window.addEventListener("popstate", handler);
// clean up
return () => {
window.removeEventListener("popstate", handler);
};
}, []);
const navigate = (to: string) => {
window.history.pushState({}, "", to);
return setCurrentPath(to);
};
return (
<NavigationContext.Provider value={{ currentPath, navigate }}>
{children}
</NavigationContext.Provider>
);
};
export { NavigationProvider };
export default NavigationContext;
The `NavigationProvider` component handles updating the `currentPath` based on browser history changes. The `useEffect` hook ensures that the currentPath is synchronized with the browser's history, effectively enabling navigation without full page reloads.
The navigate function uses window.history.pushState to manipulate the browser's history stack, allowing seamless transitions between routes.
We will use `NavigationProvider` within `main.tsx` to make sure our application has access to the navigation system we build.
main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import { NavigationProvider } from "./context/navigation.tsx";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<NavigationProvider>
<App />
</NavigationProvider>
</StrictMode>
);
Step 2. Abstract Context with `useNavigation` Hook
To abstract the complexities of accessing the `NavigationContext`, we introduce the `useNavigation` hook.
useNavigation.tsx
import { useContext } from "react";
import NavigationContext from "../context/navigation";
const useNavigation = () => {
return useContext(NavigationContext);
};
export default useNavigation;
This hook simply extracts the currentPath and navigate function from the NavigationContext through useContext, providing a convenient way to interact with the navigation state within our components without having to repetitively import `NavigationContext` and `useContext` when needing to access the `currentPath` variable or `navigate` function.
Step 3. Create the `Route` Component
The `Route` component serves as the building block for defining our application routes.
领英推荐
Route.tsx
import React from "react";
import useNavigation from "../hooks/useNavigation";
interface RouteProps {
path: string;
children: React.ReactNode;
}
const Route = ({ path, children }: RouteProps) => {
const { currentPath } = useNavigation();
if (path === currentPath) {
return children;
}
return null;
};
export default Route;
This component, utilizing the `useNavigation` hook, conditionally renders its children based on the current path. If the path property matches the `currentPath`, the children are displayed; otherwise, nothing is rendered.
Step 4. Map Application Routes
I mapped my routes in `App.tsx` to provide the structure of the application routes.
App.tsx
import Route from "./components/Route";
import Link from "./components/Link";
function App() {
return (
<>
<Route path="/">
<h1>Hello, World! I am the root route.</h1>
</Route>
<Route path="/lorem">
<p>Lorem ipsum...</p>
</Route>
</>
);
}
export default App;
Here, we use the Route component to define two routes: one for the root path (/) and another for a /lorem path. Each route is responsible for rendering the appropriate content when navigated to.
Step 5. Enable Navigation with `Link` Component
The `Link` component will be responsible for facilitating navigation within our application.
Link.tsx
import React, { PropsWithChildren } from "react";
import useNavigation from "../hooks/useNavigation";
interface LinkProps extends React.HTMLAttributes<HTMLAnchorElement> {
to: string;
className?: string;
}
const Link = ({ to, children, className }: PropsWithChildren<LinkProps>) => {
const { navigate } = useNavigation();
const handleClick = (
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) => {
if (event.metaKey || event.ctrlKey) {
return;
}
event.preventDefault();
navigate(to);
};
return (
<a onClick={handleClick} href={to}>
{children}
</a>
);
};
export default Link;
The Link component utilizes the `navigate` function from the `useNavigation` hook to update the `currentPath` when clicked. It also prevents the default anchor tag behavior, ensuring that navigation doesn't trigger a full page reload, while allowing users to open links in new tabs using Ctrl+click or Cmd+click.
Wrapping Up
By integrating these components within your React application, you'll be able to create a simple yet effective routing solution without the overhead of a full-fledged library. This approach emphasizes clean code, maintainability, and control over your application's routing logic, allowing you to customize the navigation experience to perfectly fit your needs.
Want to see this code in action? Check out my React component-library repo where I implement this solution.