Routing in React with React Router and TypeScript – Part 1: Fundamentals with Vite
Learn how to set up React Router with a TypeScript React app created using Vite. We’ll build a small multi-page layout with navigation, 404 page, and a clean folder structure that will scale to real-world apps.
When you build anything beyond a tiny React widget, routing becomes one of the first decisions you have to get right. URL structure, navigation, layout composition, and data loading all flow from your routing setup.
In this series, we’ll build a realistic React app using React Router and TypeScript, scaffolded with Vite. We’ll go from fundamentals to advanced patterns used in production apps.
In Part 1, we’ll focus on:
- Creating a Vite + React + TypeScript project
- Installing and configuring React Router v6+
- Creating simple pages:
Home,About,Contact - Building a shared layout with navigation
- Adding a 404 Not Found route
- Organizing files so we can grow this into a real-world app in later parts
By the end, you’ll have a clean base that we’ll extend in the next articles.
1. Create a Vite + React + TypeScript project
We’ll use Vite’s official template for React + TypeScript.
From a terminal, run:
# Create the project
npm create vite@latest my-react-router-app -- --template react-ts
cd my-react-router-app
# Install dependencies
npm install
Then start the dev server:
npm run dev
Open the URL printed in the terminal (usually http://localhost:5173) and confirm you see the default Vite + React + TS starter.
At this point, the relevant files are:
index.html– single HTML entry pointsrc/main.tsx– React entry that hydrates the appsrc/App.tsx– main component rendered bymain.tsxsrc/assets– default logo images
We’ll now integrate React Router into this setup.
2. Install and wire up React Router
Install React Router DOM (v6+):
npm install react-router-dom
2.1. Replace App.tsx with a router-based structure
React Router v6 favors route objects and createBrowserRouter / RouterProvider (data APIs) as the core primitives. That’s also the style that scales best to real-world apps, so we’ll use it from the beginning.
Let’s refactor our entry point to use a router.
In src/main.tsx, replace the contents with:
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { router } from "./router";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
Then create a new file src/router.tsx:
import { createBrowserRouter } from "react-router-dom";
import { AppLayout } from "./routes/AppLayout";
import { HomePage } from "./routes/HomePage";
import { AboutPage } from "./routes/AboutPage";
import { ContactPage } from "./routes/ContactPage";
import { NotFoundPage } from "./routes/NotFoundPage";
export const router = createBrowserRouter([
{
path: "/",
element: <AppLayout />,
errorElement: <NotFoundPage />,
children: [
{ index: true, element: <HomePage /> },
{ path: "about", element: <AboutPage /> },
{ path: "contact", element: <ContactPage /> },
],
},
]);
A few important points:
- We will treat
AppLayoutas our app shell – the shared layout that wraps child routes. - We use
index: truefor the home route (/), which is the “default” child of the root. - We define simple
/aboutand/contactroutes. errorElementis a place to render when something goes wrong while loading this route tree. We’ll reuse ourNotFoundPagethere.
We’re now missing the actual route components. Let’s create them.
3. Create pages and a shared layout
We’ll keep our route components under src/routes. This keeps them close to routing concerns and makes it easy to see what’s navigable in the app.
Create the folder:
mkdir src/routes
3.1. AppLayout – the shared shell
Create src/routes/AppLayout.tsx:
import { Outlet } from "react-router-dom";
import { MainNav } from "../shared/MainNav";
export const AppLayout: React.FC = () => {
return (
<div className="app-root">
<MainNav />
<main className="app-main">
<Outlet />
</main>
</div>
);
};
Key ideas:
Outletis where the child routes (Home, About, Contact) will render.MainNavis our navigation bar. We’ll create it insrc/shared.
Create src/shared/MainNav.tsx:
import { NavLink } from "react-router-dom";
import "./MainNav.css";
export const MainNav: React.FC = () => {
return (
<header className="main-nav">
<div className="main-nav__brand">My React Router App</div>
<nav className="main-nav__links">
<NavLink
to="/"
end
className={({ isActive }) =>
isActive ? "main-nav__link main-nav__link--active" : "main-nav__link"
}
>
Home
</NavLink>
<NavLink
to="/about"
className={({ isActive }) =>
isActive ? "main-nav__link main-nav__link--active" : "main-nav__link"
}
>
About
</NavLink>
<NavLink
to="/contact"
className={({ isActive }) =>
isActive ? "main-nav__link main-nav__link--active" : "main-nav__link"
}
>
Contact
</NavLink>
</nav>
</header>
);
};
This uses NavLink to render links that know whether they’re active so we can style them.
You can add minimal styling in src/shared/MainNav.css:
.main-nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1.5rem;
background: #111827;
color: #e5e7eb;
}
.main-nav__brand {
font-weight: 600;
}
.main-nav__links {
display: flex;
gap: 1rem;
}
.main-nav__link {
color: #e5e7eb;
text-decoration: none;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.95rem;
}
.main-nav__link--active {
background: #2563eb;
}
And add a few base styles in src/index.css (or keep Vite’s defaults and just add):
.app-root {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.app-main {
padding: 1.5rem;
}
3.2. Simple page components
Create src/routes/HomePage.tsx:
export const HomePage: React.FC = () => {
return (
<section>
<h1>Welcome</h1>
<p>
This is the home page of our React Router + TypeScript application
created with Vite.
</p>
<p>
In later parts of this series, we'll evolve this into a
realistic app with nested routes, data loading, and authentication.
</p>
</section>
);
};
Create src/routes/AboutPage.tsx:
export const AboutPage: React.FC = () => {
return (
<section>
<h1>About</h1>
<p>
This demo app is built to showcase React Router best practices with
TypeScript and Vite.
</p>
<p>
The goal is to keep the structure close to what you'd see in a
production app while still being small enough to understand.
</p>
</section>
);
};
Create src/routes/ContactPage.tsx:
export const ContactPage: React.FC = () => {
return (
<section>
<h1>Contact</h1>
<p>
In a real application, this page could contain a contact form that
posts to your backend or integrates with a service like SendGrid.
</p>
<p>For now, we'll keep it simple.</p>
</section>
);
};
And finally, a NotFoundPage in src/routes/NotFoundPage.tsx:
import { Link } from "react-router-dom";
export const NotFoundPage: React.FC = () => {
return (
<section>
<h1>404 – Page Not Found</h1>
<p>The page you're looking for does not exist.</p>
<p>
<Link to="/">Go back home</Link>
</p>
</section>
);
};
Now you have a simple multi-page app driven by React Router.
4. TypeScript and best practices from day one
We’ve already been using TypeScript simply by writing .tsx files. To keep things clean as this app grows, here are a few best practices we’ll stick to throughout the series:
- Use functional components typed with
React.FC(or no explicit annotation when inference is enough). - Keep routing-related components under
src/routesand shared UI undersrc/sharedorsrc/components. - Avoid anonymous inline components inside your route definitions. Instead, import them from their own files. This keeps routes tree readable and components testable.
- Use
NavLinkfor navigation UI, notLink, when you need active state styling. - Keep the route config close to the app entry (
src/router.tsx), so it’s easy to see the app’s structure at a glance.
Later parts will introduce real-world concerns: layout nesting, data loading, mutations, and auth—but we won’t have to change this foundation. We’ll just add to it.
5. Quick recap and what’s next
At this point, you should have:
- A Vite + React + TypeScript project
- React Router DOM installed and wired up via
RouterProvider - A basic route tree with
/,/about,/contact - A shared layout and navigation bar
- A friendly 404 page
In Part 2, we’ll:
- Add a
/dashboardsection with nested routes (e.g./dashboard/overview,/dashboard/reports) - Introduce layout routes for sections of the app
- Use URL parameters and
useParamsfor more dynamic pages
Related Articles
Routing in React with React Router and TypeScript – Part 5: Architecture and Best Practices
Bring everything together with a production-ready architecture for React Router + TypeScript apps built with Vite. Learn about feature-first structure, lazy routes, testing, and common pitfalls.
Routing in React with React Router and TypeScript – Part 4: Auth and Protected Routes
Add authentication and protected routes to your React Router + TypeScript app. We’ll build a simple auth flow with login, logout, and role-based access inside the Vite-powered dashboard.
Full Stack Insights
Software Engineer
Passionate about software development, architecture, and sharing knowledge.
Quick Links
Full Stack Insights
Software Engineer
Passionate about software development, architecture, and sharing knowledge with the community.