React Routing Mastery: From React Router to TanStack Router
Building single-page applications with powerful navigation and type-safe routing solutions
Introduction: The Foundation of Modern SPAs
Single-page applications (SPAs) need sophisticated routing systems to provide seamless navigation experiences without full page reloads. React Router has been the go-to solution for years, while TanStack Router represents the next evolution with type-safe, file-based routing.
Understanding routing is crucial because it determines how users navigate through your application, how URLs map to components, and how data flows between different views. A well-implemented routing system creates intuitive user experiences and maintainable code architecture.
This guide covers both React Router for traditional routing needs and TanStack Router for type-safe, modern applications, along with practical examples of forms and controlled components that commonly work alongside routing systems.
Chapter 1: React Router Crash Course
Setting Up React Router
React Router uses a component-based approach where routes are defined as React components.
// Installation
npm install react-router-dom
// Basic setup in main.jsx or App.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import {
createBrowserRouter,
RouterProvider,
BrowserRouter,
Routes,
Route,
Link
} from 'react-router-dom'
// Method 1: Using createBrowserRouter (recommended)
const router = createBrowserRouter([
{
path: "/",
element: <Home />,
},
{
path: "/about",
element: <About />,
},
{
path: "/contact",
element: <Contact />,
},
])
ReactDOM.createRoot(document.getElementById('root')).render(
<RouterProvider router={router} />
)
Basic Components and Navigation
// Method 2: Using BrowserRouter with Routes
function App() {
return (
<BrowserRouter>
{/* Navigation Bar */}
<nav className="navbar">
<div className="nav-brand">
<Link to="/">MyApp</Link>
</div>
<ul className="nav-links">
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/products">Products</Link></li>
<li><Link to="/contact">Contact</Link></li>
</ul>
</nav>
{/* Route Definitions */}
<main className="main-content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
</BrowserRouter>
)
}
// Page Components
function Home() {
return (
<div>
<h1>Welcome to Our Store</h1>
<p>Discover amazing products and services!</p>
<Link to="/products" className="cta-button">
Browse Products
</Link>
</div>
)
}
function Products() {
const products = [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Phone', price: 599 },
{ id: 3, name: 'Headphones', price: 199 }
]
return (
<div>
<h1>Our Products</h1>
<div className="products-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>${product.price}</p>
<Link to={`/products/${product.id}`}>
View Details
</Link>
</div>
))}
</div>
</div>
)
}
Dynamic Routes and Parameters
import { useParams, useNavigate, useLocation } from 'react-router-dom'
function ProductDetail() {
const { id } = useParams()
const navigate = useNavigate()
const location = useLocation()
const [product, setProduct] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
// Simulate API call
const fetchProduct = async () => {
setLoading(true)
try {
// Mock product data
const productData = {
1: { id: 1, name: 'Laptop', price: 999, description: 'High-performance laptop' },
2: { id: 2, name: 'Phone', price: 599, description: 'Latest smartphone' },
3: { id: 3, name: 'Headphones', price: 199, description: 'Wireless headphones' }
}
setTimeout(() => {
setProduct(productData[id] || null)
setLoading(false)
}, 500)
} catch (error) {
setLoading(false)
}
}
fetchProduct()
}, [id])
if (loading) {
return <div className="loading">Loading product...</div>
}
if (!product) {
return (
<div className="not-found">
<h2>Product not found</h2>
<button onClick={() => navigate('/products')}>
Back to Products
</button>
</div>
)
}
return (
<div className="product-detail">
<button onClick={() => navigate(-1)} className="back-button">
← Back
</button>
<div className="product-info">
<h1>{product.name}</h1>
<p className="price">${product.price}</p>
<p className="description">{product.description}</p>
<div className="actions">
<button className="add-to-cart">Add to Cart</button>
<button
onClick={() => navigate('/contact', {
state: { product: product.name }
})}
>
Ask Question
</button>
</div>
</div>
</div>
)
}
Nested Routes and Layouts
// Layout component with Outlet
import { Outlet, Link, useLocation } from 'react-router-dom'
function DashboardLayout() {
const location = useLocation()
return (
<div className="dashboard">
<aside className="sidebar">
<nav className="sidebar-nav">
<Link
to="/dashboard"
className={location.pathname === '/dashboard' ? 'active' : ''}
>
Overview
</Link>
<Link
to="/dashboard/profile"
className={location.pathname === '/dashboard/profile' ? 'active' : ''}
>
Profile
</Link>
<Link
to="/dashboard/settings"
className={location.pathname === '/dashboard/settings' ? 'active' : ''}
>
Settings
</Link>
</nav>
</aside>
<main className="dashboard-content">
<Outlet /> {/* Child routes render here */}
</main>
</div>
)
}
// Router configuration with nested routes
const router = createBrowserRouter([
{
path: "/",
element: <Home />,
},
{
path: "/dashboard",
element: <DashboardLayout />,
children: [
{
path: "",
element: <DashboardOverview />,
},
{
path: "profile",
element: <Profile />,
},
{
path: "settings",
element: <Settings />,
},
],
},
])
function DashboardOverview() {
return (
<div>
<h1>Dashboard Overview</h1>
<div className="stats-grid">
<div className="stat-card">
<h3>Total Orders</h3>
<p>1,234</p>
</div>
<div className="stat-card">
<h3>Revenue</h3>
<p>$12,345</p>
</div>
</div>
</div>
)
}
Chapter 2: Controlled Forms and Components
Understanding Controlled Components
Controlled components sync form inputs with React state, providing a single source of truth.
// Basic controlled form
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: ''
})
const [errors, setErrors] = useState({})
const [isSubmitting, setIsSubmitting] = useState(false)
const navigate = useNavigate()
const location = useLocation()
// Pre-fill form if coming from product page
useEffect(() => {
if (location.state?.product) {
setFormData(prev => ({
...prev,
subject: `Question about ${location.state.product}`
}))
}
}, [location.state])
const handleChange = (e) => {
const { name, value } = e.target
setFormData(prev => ({
...prev,
[name]: value
}))
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ''
}))
}
}
const validateForm = () => {
const newErrors = {}
if (!formData.name.trim()) {
newErrors.name = 'Name is required'
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required'
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid'
}
if (!formData.message.trim()) {
newErrors.message = 'Message is required'
}
return newErrors
}
const handleSubmit = async (e) => {
e.preventDefault()
const validationErrors = validateForm()
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors)
return
}
setIsSubmitting(true)
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000))
// Show success and redirect
alert('Message sent successfully!')
navigate('/', { replace: true })
} catch (error) {
setErrors({ submit: 'Failed to send message. Please try again.' })
} finally {
setIsSubmitting(false)
}
}
return (
<div className="contact-page">
<h1>Contact Us</h1>
<form onSubmit={handleSubmit} className="contact-form">
<div className="form-group">
<label htmlFor="name">Name *</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className={errors.name ? 'error' : ''}
disabled={isSubmitting}
/>
{errors.name && <span className="error-text">{errors.name}</span>}
</div>
<div className="form-group">
<label htmlFor="email">Email *</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
disabled={isSubmitting}
/>
{errors.email && <span className="error-text">{errors.email}</span>}
</div>
<div className="form-group">
<label htmlFor="subject">Subject</label>
<input
type="text"
id="subject"
name="subject"
value={formData.subject}
onChange={handleChange}
disabled={isSubmitting}
/>
</div>
<div className="form-group">
<label htmlFor="message">Message *</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
rows="5"
className={errors.message ? 'error' : ''}
disabled={isSubmitting}
/>
{errors.message && <span className="error-text">{errors.message}</span>}
</div>
{errors.submit && (
<div className="error-banner">{errors.submit}</div>
)}
<div className="form-actions">
<button
type="button"
onClick={() => navigate(-1)}
disabled={isSubmitting}
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</div>
</form>
</div>
)
}
Advanced Form Patterns
// Multi-step form with routing
function MultiStepForm() {
const [currentStep, setCurrentStep] = useState(1)
const [formData, setFormData] = useState({
// Step 1: Personal Info
firstName: '',
lastName: '',
email: '',
phone: '',
// Step 2: Address
address: '',
city: '',
state: '',
zipCode: '',
// Step 3: Preferences
newsletter: false,
notifications: true,
interests: []
})
const navigate = useNavigate()
const handleNext = () => {
if (currentStep < 3) {
setCurrentStep(currentStep + 1)
// Update URL to reflect current step
navigate(`/signup/step-${currentStep + 1}`, { replace: true })
}
}
const handlePrevious = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1)
navigate(`/signup/step-${currentStep - 1}`, { replace: true })
}
}
const handleSubmit = async () => {
try {
await submitForm(formData)
navigate('/welcome')
} catch (error) {
console.error('Form submission failed:', error)
}
}
return (
<div className="multi-step-form">
<div className="progress-bar">
<div
className="progress"
style={{ width: `${(currentStep / 3) * 100}%` }}
/>
</div>
<div className="step-indicators">
{[1, 2, 3].map(step => (
<div
key={step}
className={`step ${currentStep === step ? 'active' : ''} ${currentStep > step ? 'completed' : ''}`}
>
{step}
</div>
))}
</div>
{currentStep === 1 && (
<PersonalInfoStep
data={formData}
onChange={setFormData}
onNext={handleNext}
/>
)}
{currentStep === 2 && (
<AddressStep
data={formData}
onChange={setFormData}
onNext={handleNext}
onPrevious={handlePrevious}
/>
)}
{currentStep === 3 && (
<PreferencesStep
data={formData}
onChange={setFormData}
onPrevious={handlePrevious}
onSubmit={handleSubmit}
/>
)}
</div>
)
}
function PersonalInfoStep({ data, onChange, onNext }) {
const handleChange = (field, value) => {
onChange(prev => ({ ...prev, [field]: value }))
}
const canProceed = data.firstName && data.lastName && data.email
return (
<div className="form-step">
<h2>Personal Information</h2>
<div className="form-row">
<input
type="text"
placeholder="First Name"
value={data.firstName}
onChange={(e) => handleChange('firstName', e.target.value)}
/>
<input
type="text"
placeholder="Last Name"
value={data.lastName}
onChange={(e) => handleChange('lastName', e.target.value)}
/>
</div>
<input
type="email"
placeholder="Email Address"
value={data.email}
onChange={(e) => handleChange('email', e.target.value)}
/>
<input
type="tel"
placeholder="Phone Number"
value={data.phone}
onChange={(e) => handleChange('phone', e.target.value)}
/>
<button
onClick={onNext}
disabled={!canProceed}
className="next-button"
>
Next Step
</button>
</div>
)
}
Chapter 3: TanStack Router for Robust Routing
Introduction to TanStack Router
TanStack Router provides type-safe, file-based routing with advanced features.
# Installation
npm install @tanstack/router @tanstack/router-devtools
# Install CLI for code generation
npm install -D @tanstack/router-cli
Basic TanStack Router Setup
// routeTree.gen.ts (auto-generated)
import { createFileRoute, createRootRoute } from '@tanstack/router'
// Root route
export const rootRoute = createRootRoute({
component: RootComponent
})
function RootComponent() {
return (
<div className="app">
<nav className="navbar">
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/products">Products</Link>
</nav>
<main>
<Outlet />
</main>
</div>
)
}
// File-based routes
// src/routes/index.tsx
export const indexRoute = createFileRoute('/')({
component: HomePage
})
function HomePage() {
return (
<div>
<h1>Welcome to TanStack Router</h1>
<p>Type-safe routing for React applications</p>
</div>
)
}
// src/routes/products/index.tsx
export const productsIndexRoute = createFileRoute('/products/')({
component: ProductsPage,
loader: async () => {
// Type-safe data loading
const products = await fetchProducts()
return { products }
}
})
function ProductsPage() {
const { products } = productsIndexRoute.useLoaderData()
return (
<div>
<h1>Products</h1>
<div className="products-grid">
{products.map(product => (
<div key={product.id}>
<h3>{product.name}</h3>
<Link
to="/products/$productId"
params={{ productId: product.id }}
>
View Details
</Link>
</div>
))}
</div>
</div>
)
}
// src/routes/products/$productId.tsx
export const productRoute = createFileRoute('/products/$productId')({
component: ProductDetailPage,
loader: async ({ params }) => {
const product = await fetchProduct(params.productId)
return { product }
}
})
function ProductDetailPage() {
const { productId } = productRoute.useParams()
const { product } = productRoute.useLoaderData()
const navigate = productRoute.useNavigate()
return (
<div>
<button onClick={() => navigate({ to: '/products' })}>
← Back to Products
</button>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>${product.price}</p>
</div>
)
}
Advanced TanStack Router Features
// Type-safe search parameters
export const searchRoute = createFileRoute('/search')({
component: SearchPage,
validateSearch: z.object({
query: z.string().optional(),
category: z.string().optional(),
page: z.number().int().min(1).optional().default(1),
limit: z.number().int().min(1).max(100).optional().default(20)
})
})
function SearchPage() {
const navigate = searchRoute.useNavigate()
const { query, category, page, limit } = searchRoute.useSearch()
const updateSearch = (updates) => {
navigate({
search: (prev) => ({ ...prev, ...updates })
})
}
return (
<div>
<div className="search-controls">
<input
value={query || ''}
onChange={(e) => updateSearch({ query: e.target.value, page: 1 })}
placeholder="Search products..."
/>
<select
value={category || ''}
onChange={(e) => updateSearch({ category: e.target.value, page: 1 })}
>
<option value="">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
</div>
<SearchResults
query={query}
category={category}
page={page}
limit={limit}
/>
<Pagination
currentPage={page}
onPageChange={(newPage) => updateSearch({ page: newPage })}
/>
</div>
)
}
// Context and data loading
export const dashboardRoute = createFileRoute('/dashboard')({
component: DashboardLayout,
beforeLoad: async ({ context }) => {
// Check authentication
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login' })
}
},
loader: async ({ context }) => {
const user = await fetchCurrentUser(context.auth.token)
return { user }
}
})
function DashboardLayout() {
const { user } = dashboardRoute.useLoaderData()
return (
<div className="dashboard">
<header>
<h1>Welcome, {user.name}</h1>
</header>
<Outlet />
</div>
)
}
Conclusion
Modern React routing has evolved from simple page navigation to sophisticated, type-safe systems that handle complex application state and data flow. React Router remains the standard choice for most applications, providing proven patterns and extensive ecosystem support. TanStack Router represents the cutting edge with type safety, file-based routing, and advanced data loading capabilities.
Key Concepts Mastered
React Router Fundamentals: Understanding route definition, navigation, nested routes, and URL parameters provides the foundation for building navigable single-page applications.
Controlled Components: Mastering form state management and validation creates responsive user interfaces that maintain data integrity and provide excellent user experiences.
Advanced Routing: Leveraging layout components, data loading, and type-safe routing patterns enables building complex, maintainable applications with sophisticated navigation flows.
Best Practices Learned
Route Organization: Structuring routes hierarchically with proper nesting and layouts creates maintainable, scalable routing architectures.
Form State Management: Implementing controlled components with proper validation and error handling ensures robust user input processing.
Type Safety: Using TypeScript with TanStack Router catches routing errors at compile time and provides excellent developer experience through autocomplete and type checking.
Whether building traditional SPAs with React Router or next-generation applications with TanStack Router, understanding these routing concepts enables creating sophisticated, user-friendly web applications that provide seamless navigation experiences and robust data management capabilities.