React Router from Scratch: Mapping Every API Back to a 60-Line HashRouter
Many developers treat React Router as opaque framework magic. Mapping every API back to a 60-line hashchange router collapses that abstraction: HashRouter is just addEventListener, Link is just preventDefault, and Outlet is just a div that gets its children swapped. Understanding this makes debugging route mismatches, nested outlet failures, and lazy-loading timing trivial rather than mysterious.
A step-by-step walkthrough builds a React Router application while keeping a 60-line handwritten HashRouter as the mental model. Every React Router concept — HashRouter wrapping hashchange, Link intercepting anchor clicks, Routes replacing a routing table lookup, useParams parsing the hash, Outlet serving as a nested mount point — is traced back to its vanilla JavaScript equivalent. The correspondence demystifies the library: there is no magic, only the same browser events and DOM manipulation packaged as declarative components and hooks.
The project structure demonstrates lazy loading with React.lazy and Suspense, showing exactly when dynamic imports fire and why synchronous imports bloat the initial bundle. A full click-to-render simulation walks through hashchange firing, Routes iterating, Suspense showing fallbacks, and nested Outlets resolving — all without a single HTTP request. A final concept-mapping table and a ten-question checklist serve as a self-assessment for anyone who wants to stop treating React Router as a black box.
React Router's design is fundamentally a 1:1 declarative wrapper around three browser primitives: hashchange events, anchor tag interception, and location.hash manipulation. The library adds no new browser capability — it only repackages existing ones as React components.
The decision to alias HashRouter as Router is a practical abstraction seam: swapping to BrowserRouter later requires changing only the import statement, not the component tree, because both conform to the same Router interface.
Lazy loading's real cost is not the download but the module-level code execution at import time. A lazily loaded component still runs all its top-level statements the moment the file arrives, which means heavy initialization in module scope defeats the purpose of code splitting.
The nested route design — parent Route wrapping child Routes, with Outlet as the insertion point — mirrors the DOM tree itself. This makes route configuration visually isomorphic to the component hierarchy, reducing the mental gap between routing logic and rendered output.
SPA routing's defining characteristic is not client-side rendering but the absence of HTTP requests during navigation. The entire flow from Link click to component swap happens without a single network round-trip to the server, which is what makes it feel instantaneous compared to multi-page apps.