Skip to main content

Command Palette

Search for a command to run...

React Routing Mastery: From React Router to TanStack Router

Building single-page applications with powerful navigation and type-safe routing solutions

Published
10 min read

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.