Finding My Bearings#
Before this semester, the only frontend work I had done was with Thymeleaf — server-rendered pages where the server decides what the browser receives, and navigation means a full round trip. React is a different model entirely, and routing is where that difference is most visible.
In a single-page application, the server serves one HTML file and stays out of the way. The browser never reloads. React Router intercepts navigation events and swaps out components instead, which means the application needs to own its own routing logic entirely.
That shift in responsibility took a little time to internalise.
AppRoutes as a Contract#
One decision I made early was to define all routes in a dedicated AppRoutes component before most pages existed. This was not something covered in class — it was a way of making the scope of the application concrete and visible before building into it.
Having a complete route map early meant I could see what pages were needed, in what hierarchy, and which ones required authentication. It functioned more as a planning tool than a technical requirement, and it helped me avoid scope creep by making it obvious when something was not on the list.
Structure#
The root route renders App, which provides the shared layout — header, footer, and an Outlet that renders whichever child route matched. Every page in the application is a child of that root.
Public routes sit at the top level. Protected routes are grouped under a single pathless ProtectedRoute:
<Route element={<ProtectedRoute />}>
<Route path="account" element={<Account />} />
<Route path="characters">
<Route path="create" element={<CharacterCreate />} />
<Route path=":id" element={<CharacterDetail />} />
<Route path=":id/edit" element={<CharacterEdit />} />
</Route>
</Route>The characters path appears twice in the route tree — once as a public overview anyone can visit, and once as a protected group for create, detail, and edit. The overview is intentionally public; there is no reason to require login just to see that the feature exists. The overview page uses conditional rendering to show a different view depending on whether the user is logged in — guests see enough to understand the scope of the application, authenticated users see their data.
The pathless route containing ProtectedRoute has no path of its own. It contributes no URL segment — it only wraps its children with an auth check.
ProtectedRoute#
export default function ProtectedRoute() {
const { isLoggedIn } = useAuth();
const location = useLocation();
if (!isLoggedIn) {
return <Navigate to="/login" state={{ from: location.pathname, errorMessage: 'Login to access this content' }} />;
}
return <Outlet />;
}If the user is not authenticated, they are redirected to the login page. The current path and an error message are forwarded as navigation state. The login page reads that state and surfaces the message through StatusMessage, so the user understands why they landed there rather than where they intended to go.
ProtectedRoute renders Outlet when the check passes — it has no knowledge of which page follows. That is the child route’s concern.
Reflections#
Coming from server-rendered pages, defining routes as a tree of components rather than files on a server was a different way of thinking about navigation. Once the mental model clicked — that the URL is just a reflection of which components are active — the structure started to feel natural. Building AppRoutes first, before most pages existed, turned out to be the right instinct.
