Fumadocs + Code Hike
Frontend

React Router

Biblioteca de roteamento declarativo para React

O que é React Router?

React Router é a biblioteca padrão para roteamento em aplicações React. Permite navegação declarativa, roteamento aninhado e gerenciamento de histórico do navegador de forma elegante e performática.

Por que utilizamos React Router na IngenioLab?

  • Padrão da comunidade: Biblioteca mais usada para roteamento
  • Roteamento declarativo: Components como rotas
  • Nested routing: Layouts aninhados naturalmente
  • History management: Controle total do histórico
  • Code splitting: Integração com lazy loading
  • Data loading: Carregamento de dados por rota

Instalação

# React Router v6
npm install react-router-dom
# Para TypeScript
npm install -D @types/react-router-dom

Configuração Base

1. Setup da aplicação:

// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
)

2. Estrutura de rotas básica:

// src/App.tsx
import { Routes, Route } from 'react-router-dom'
import { Layout } from './components/Layout'
import { Home } from './pages/Home'
import { About } from './pages/About'
import { Users } from './pages/Users'
import { UserDetail } from './pages/UserDetail'
import { NotFound } from './pages/NotFound'
export default function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="users" element={<Users />} />
<Route path="users/:userId" element={<UserDetail />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
)
}

3. Layout com Outlet:

// src/components/Layout.tsx
import { Outlet, Link, useLocation } from 'react-router-dom'
export const Layout = () => {
const location = useLocation()
return (
<div className="layout">
<nav className="navbar">
<Link
to="/"
className={location.pathname === '/' ? 'active' : ''}
>
Home
</Link>
<Link
to="/about"
className={location.pathname === '/about' ? 'active' : ''}
>
About
</Link>
<Link
to="/users"
className={location.pathname.startsWith('/users') ? 'active' : ''}
>
Users
</Link>
</nav>
<main className="main-content">
<Outlet />
</main>
</div>
)
}

Hooks Principais

1. useNavigate - Navegação programática:

// src/components/UserActions.tsx
import { useNavigate } from 'react-router-dom'
export const UserActions = ({ userId }: { userId: string }) => {
const navigate = useNavigate()
const goToUser = () => {
navigate(`/users/${userId}`)
}
const goToEdit = () => {
navigate(`/users/${userId}/edit`, {
state: { from: 'user-list' }
})
}
const goBack = () => {
navigate(-1) // Voltar uma página
}
const redirectToLogin = () => {
navigate('/login', {
replace: true, // Substituir no histórico
state: { redirectTo: window.location.pathname }
})
}
return (
<div>
<button onClick={goToUser}>Ver Usuário</button>
<button onClick={goToEdit}>Editar</button>
<button onClick={goBack}>Voltar</button>
</div>
)
}

2. useParams - Parâmetros da URL:

// src/pages/UserDetail.tsx
import { useParams, useNavigate } from 'react-router-dom'
import { useUser } from '@/hooks/useUsers'
interface UserParams {
userId: string
}
export const UserDetail = () => {
const { userId } = useParams<UserParams>()
const navigate = useNavigate()
const { data: user, isLoading, error } = useUser(userId!)
if (isLoading) return <div>Carregando...</div>
if (error) return <div>Erro: {error.message}</div>
if (!user) {
navigate('/users', { replace: true })
return null
}
return (
<div className="user-detail">
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
<p>ID: {userId}</p>
</div>
)
}

3. useSearchParams - Query parameters:

// src/pages/Users.tsx
import { useSearchParams } from 'react-router-dom'
import { useUsers } from '@/hooks/useUsers'
export const Users = () => {
const [searchParams, setSearchParams] = useSearchParams()
// Ler parâmetros atuais
const page = Number(searchParams.get('page')) || 1
const search = searchParams.get('search') || ''
const role = searchParams.get('role') || ''
const { data: users } = useUsers({ page, search, role })
const updateSearch = (newSearch: string) => {
setSearchParams(prev => {
if (newSearch) {
prev.set('search', newSearch)
prev.set('page', '1') // Reset page ao buscar
} else {
prev.delete('search')
}
return prev
})
}
const updatePage = (newPage: number) => {
setSearchParams(prev => {
prev.set('page', newPage.toString())
return prev
})
}
return (
<div>
<input
type="text"
value={search}
onChange={(e) => updateSearch(e.target.value)}
placeholder="Buscar usuários..."
/>
<select
value={role}
onChange={(e) => setSearchParams(prev => {
if (e.target.value) {
prev.set('role', e.target.value)
} else {
prev.delete('role')
}
return prev
})}
>
<option value="">Todos os roles</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
{/* Lista de usuários */}
<UserList users={users} />
{/* Paginação */}
<Pagination
currentPage={page}
onPageChange={updatePage}
/>
</div>
)
}

4. useLocation - Informações da localização:

// src/hooks/usePageTracking.ts
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
export const usePageTracking = () => {
const location = useLocation()
useEffect(() => {
// Analytics tracking
window.gtag?.('config', 'GA_MEASUREMENT_ID', {
page_path: location.pathname + location.search
})
// Log para debug
console.log('Page changed:', {
pathname: location.pathname,
search: location.search,
state: location.state
})
}, [location])
}
// src/components/Breadcrumbs.tsx
import { useLocation, Link } from 'react-router-dom'
export const Breadcrumbs = () => {
const location = useLocation()
const pathSegments = location.pathname
.split('/')
.filter(segment => segment !== '')
return (
<nav className="breadcrumbs">
<Link to="/">Home</Link>
{pathSegments.map((segment, index) => {
const path = `/${pathSegments.slice(0, index + 1).join('/')}`
return (
<span key={path}>
{' / '}
<Link to={path}>{segment}</Link>
</span>
)
})}
</nav>
)
}

Rotas Aninhadas

1. Layout aninhado:

// src/App.tsx
export default function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
{/* Dashboard com sub-rotas */}
<Route path="dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="analytics" element={<Analytics />} />
<Route path="settings" element={<Settings />} />
<Route path="users" element={<DashboardUsers />} />
</Route>
{/* Users com sub-rotas */}
<Route path="users">
<Route index element={<Users />} />
<Route path=":userId" element={<UserDetail />} />
<Route path=":userId/edit" element={<EditUser />} />
<Route path="create" element={<CreateUser />} />
</Route>
</Route>
</Routes>
)
}

2. Dashboard Layout:

// src/components/DashboardLayout.tsx
import { Outlet, NavLink } from 'react-router-dom'
export const DashboardLayout = () => {
return (
<div className="dashboard">
<aside className="sidebar">
<nav>
<NavLink
to="/dashboard"
end
className={({ isActive }) => isActive ? 'active' : ''}
>
Overview
</NavLink>
<NavLink
to="/dashboard/analytics"
className={({ isActive }) => isActive ? 'active' : ''}
>
Analytics
</NavLink>
<NavLink
to="/dashboard/settings"
className={({ isActive }) => isActive ? 'active' : ''}
>
Settings
</NavLink>
</nav>
</aside>
<main className="dashboard-main">
<Outlet />
</main>
</div>
)
}

Proteção de Rotas

1. Protected Route Component:

// src/components/ProtectedRoute.tsx
import { Navigate, useLocation } from 'react-router-dom'
import { useAuth } from '@/hooks/useAuth'
interface ProtectedRouteProps {
children: React.ReactNode
roles?: string[]
}
export const ProtectedRoute = ({ children, roles }: ProtectedRouteProps) => {
const { user, isAuthenticated } = useAuth()
const location = useLocation()
if (!isAuthenticated) {
return (
<Navigate
to="/login"
state={{ from: location }}
replace
/>
)
}
if (roles && !roles.includes(user?.role)) {
return (
<Navigate
to="/unauthorized"
replace
/>
)
}
return <>{children}</>
}

2. Uso em rotas:

// src/App.tsx
import { ProtectedRoute } from './components/ProtectedRoute'
export default function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="login" element={<Login />} />
{/* Rotas protegidas */}
<Route
path="dashboard/*"
element={
<ProtectedRoute>
<DashboardRoutes />
</ProtectedRoute>
}
/>
{/* Rota apenas para admins */}
<Route
path="admin/*"
element={
<ProtectedRoute roles={['admin']}>
<AdminRoutes />
</ProtectedRoute>
}
/>
</Route>
</Routes>
)
}

Lazy Loading

1. Componentes lazy:

// src/App.tsx
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
// Lazy imports
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Users = lazy(() => import('./pages/Users'))
const Analytics = lazy(() => import('./pages/Analytics'))
export default function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route
path="dashboard/*"
element={
<Suspense fallback={<div>Carregando Dashboard...</div>}>
<Dashboard />
</Suspense>
}
/>
<Route
path="users/*"
element={
<Suspense fallback={<div>Carregando Users...</div>}>
<Users />
</Suspense>
}
/>
<Route
path="analytics"
element={
<Suspense fallback={<div>Carregando Analytics...</div>}>
<Analytics />
</Suspense>
}
/>
</Route>
</Routes>
)
}

2. Loading component reutilizável:

// src/components/RouteLoader.tsx
interface RouteLoaderProps {
children: React.ReactNode
fallback?: React.ReactNode
}
export const RouteLoader = ({
children,
fallback = <div className="loading">Carregando...</div>
}: RouteLoaderProps) => {
return (
<Suspense fallback={fallback}>
{children}
</Suspense>
)
}

Data Loading

1. Loader Pattern:

// src/pages/UserDetail.tsx
import { useEffect, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
export const UserDetail = () => {
const { userId } = useParams()
const navigate = useNavigate()
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const loadUser = async () => {
try {
setLoading(true)
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) {
if (response.status === 404) {
navigate('/users', { replace: true })
return
}
throw new Error('Failed to load user')
}
const userData = await response.json()
setUser(userData)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
if (userId) {
loadUser()
}
}, [userId, navigate])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
if (!user) return <div>User not found</div>
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}

Error Handling

1. Error Boundary para rotas:

// src/components/RouteErrorBoundary.tsx
import { Component, ErrorInfo, ReactNode } from 'react'
import { Link } from 'react-router-dom'
interface Props {
children: ReactNode
}
interface State {
hasError: boolean
error?: Error
}
export class RouteErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error): State {
return {
hasError: true,
error
}
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Route error:', error, errorInfo)
}
render() {
if (this.state.hasError) {
return (
<div className="error-page">
<h2>Oops! Algo deu errado</h2>
<p>{this.state.error?.message}</p>
<Link to="/">Voltar ao início</Link>
</div>
)
}
return this.props.children
}
}

2. 404 Page:

// src/pages/NotFound.tsx
import { Link, useLocation } from 'react-router-dom'
export const NotFound = () => {
const location = useLocation()
return (
<div className="not-found">
<h1>404 - Página não encontrada</h1>
<p>A página "{location.pathname}" não existe.</p>
<Link to="/">Voltar ao início</Link>
</div>
)
}

Formulários e Actions

1. Form com navegação:

// src/pages/CreateUser.tsx
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
export const CreateUser = () => {
const navigate = useNavigate()
const [formData, setFormData] = useState({
name: '',
email: ''
})
const [loading, setLoading] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
setLoading(true)
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
if (!response.ok) throw new Error('Failed to create user')
const newUser = await response.json()
// Redirecionar para o usuário criado
navigate(`/users/${newUser.id}`, {
state: { message: 'User created successfully' }
})
} catch (error) {
alert('Error creating user')
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="Nome"
required
/>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
placeholder="Email"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Criando...' : 'Criar Usuário'}
</button>
</form>
)
}

Performance e Otimizações

1. Route Memoization:

// src/components/OptimizedRoute.tsx
import { memo } from 'react'
import { Route } from 'react-router-dom'
interface OptimizedRouteProps {
path: string
element: React.ReactElement
}
export const OptimizedRoute = memo<OptimizedRouteProps>(({ path, element }) => (
<Route path={path} element={element} />
))

2. Navigation Guards:

// src/hooks/useNavigationGuard.ts
import { useEffect } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
export const useNavigationGuard = (
shouldBlock: boolean,
message: string = 'Você tem alterações não salvas. Deseja sair?'
) => {
const navigate = useNavigate()
const location = useLocation()
useEffect(() => {
if (!shouldBlock) return
const unblock = navigate.block?.(({ action, location: nextLocation, retry }) => {
if (window.confirm(message)) {
unblock()
retry()
}
})
return unblock
}, [shouldBlock, message, navigate])
}

Testes

1. Testes com React Testing Library:

// src/pages/__tests__/UserDetail.test.tsx
import { render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom'
import { UserDetail } from '../UserDetail'
const renderWithRouter = (component: React.ReactElement) => {
return render(
<BrowserRouter>
{component}
</BrowserRouter>
)
}
describe('UserDetail', () => {
it('should render user information', () => {
renderWithRouter(<UserDetail />)
expect(screen.getByText('Loading...')).toBeInTheDocument()
})
})