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,
});

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.