Building My First SPA with React Router
Client-side routing felt like magic until I understood what was actually happening. Here's what I learned building my first real SPA.
The Project
My first single-page application was an internal dashboard for tracking production metrics across a small garments factory. The requirements were straightforward — a few list views, a few detail pages, and some charts — but the routing was the part that intimidated me, because client-side routing felt like magic. In a traditional server-rendered application, every URL corresponds to a page on the server, and navigation is a full page reload. In a SPA, the URL changes but the page doesn't reload, and the application is responsible for deciding what to render based on the URL. That's a fundamentally different model, and understanding it was the difference between using React Router effectively and cargo-culting patterns from tutorials.
The dashboard had about eight routes, and I structured them as a flat list initially — `/dashboard`, `/orders`, `/orders/:id`, `/inventory`, `/inventory/:id`, and so on. That structure worked for the first version, but it didn't scale when the requirements grew to include nested views. An order detail page needed to show tabs for line items, shipping history, and payment status, and each tab needed its own URL so users could bookmark and share specific views. React Router's nested routing — the `Outlet` component and the nested route configuration — was the solution, and learning to use it properly was the moment React Router stopped feeling like a URL-to-component mapping and started feeling like a real navigation architecture.
React Router Basics
The core insight of client-side routing is that the URL is state, and like any state, it should drive what the UI renders. Once I internalized that — the URL is just another piece of state that React components can read and respond to — everything else about React Router fell into place. Route parameters (`:id`), query parameters (`?filter=active`), and nested routes all became different ways of structuring URL state, and the components that read that state became predictable functions of the URL. This is the same insight that makes URL search params powerful for filter state in data-heavy applications, and it's an insight I keep returning to in every SPA I build.
The part that took longest to learn was the relationship between routing and data fetching. In a server-rendered application, the data for a page is loaded on the server before the HTML is sent. In a SPA, the data is loaded on the client after the route renders, which means there's a window where the component is mounted but the data isn't there yet. Handling that window gracefully — showing a loading state, handling errors, caching data so navigation back to a previously-visited route is instant — is one of the core challenges of SPA development, and it's a challenge that frameworks like Next.js have largely solved with server components and loaders, but that you have to solve yourself in a plain SPA.
What I Learned
The biggest lesson from that first SPA was about URL design. URLs are part of the user interface — users see them, share them, bookmark them, and rely on them to navigate. A URL like `/orders/12345/line-items` is more useful than `/orders?id=12345&tab=line_items` because it's readable, hierarchical, and works with the browser's back button in a way that query-parameter-based routing doesn't. Spending time designing URLs before writing components is an investment that pays off throughout the life of the application, because URLs are much harder to change than components once users have started bookmarking them.
The other lesson was about the boundaries between routing, state, and data fetching. In my first SPA, I put too much state in React component state and not enough in the URL, which meant that refreshing the page lost the user's place, and sharing a link didn't share the view. Moving filter, sort, and pagination state into URL search params solved those problems and made the application feel more like a native web application — every state was bookmarkable, every view was shareable, and the back button worked the way users expected. That principle — "if the user would want to bookmark it, put it in the URL" — has guided every SPA I've built since.
