Step 6 — Routing with react-router-dom + Data Fetching with TanStack Query
Part 1's last step covers two libraries that always show up together: react-router-dom turns a URL into a component, and TanStack Query v5 turns a network request into cached, reactive data. This very tutorial uses both — the URL you're reading this on (/docs/react-ts-as-frontend/router-and-query) is parsed by react-router-dom, and the markdown content was fetched (well — statically bundled, see Step 3) the same way real API data is fetched elsewhere in the app.
Routes as data, not JSX trees
App.tsx declares the docs routes plainly:
<Route path="/docs" element={<DocsLayout />}>
<Route index element={<DocsIndex />} />
<Route path=":section" element={<DocsContent />} />
<Route path=":section/:page" element={<DocsContent />} />
</Route>
:section and :page are route params — placeholders that match any URL segment. DocsContent reads them back out with useParams().
useParams in a real page: TeamMembers.tsx
import { useParams } from 'react-router-dom';
export function TeamMembers() {
const { teamId: teamSlugOrId } = useParams<{ teamId: string }>();
const { team, members: teamMembers, isLoading: isTeamLoading } =
useTeamDashboard(teamSlugOrId);
// ...
}
The route for this page is declared somewhere as path="/teams/:teamId/members". Whatever segment appears where :teamId is — a slug or a UUID — useParams<{ teamId: string }>() returns it as teamSlugOrId. The generic <{ teamId: string }> tells TypeScript what shape to expect back, so teamSlugOrId is typed as string | undefined, not any.
Notice teamSlugOrId then flows straight into a custom hook, useTeamDashboard — this is the seam between routing (where am I?) and data fetching (what do I need to show?).
TanStack Query: the shape every custom hook follows
You'll trace this in full with real code in Step 11, but the shape is worth previewing now. A TanStack Query hook wraps useQuery (for reads) and useMutation (for writes) around an API call:
const tagsQuery = useQuery({
queryKey: ['tags', teamId],
queryFn: () => tagsApi.list({ team_id: teamId }),
enabled: !!teamId,
staleTime: 5 * 60 * 1000,
});
queryKeyis a cache key —['tags', teamId]means "the tags list for this specific team." ChangeteamIdand TanStack Query treats it as a different cache entry automatically.queryFnis the actual fetch — anyasyncfunction returning the data.enabled: !!teamIdskips fetching entirely untilteamIdexists — useful when a param arrives from the route a tick after the component first renders.staleTimecontrols how long cached data is considered fresh before a background refetch — no manual cache invalidation loops.
The payoff: any two components that call useTags(teamId) for the same teamId share one in-flight request and one cache entry, with loading and error states handled for you (tagsQuery.isLoading, tagsQuery.error) instead of hand-rolled useEffect + useState fetching.
Checkpoint
You've now covered every piece Part 1 set out to teach: TypeScript unions and props (Step 2), Vite + project structure (Step 3), TailwindCSS + shadcn/ui composition (Step 4), Zustand for client state (Step 5), and routing + query fundamentals (this step).
Continue to Step 7 — The Database Layer: TagRepository in Rust, where Part 2 begins: a single real feature, traced from a Postgres row to a rendered dialog.