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 v6npm install react-router-dom# Para TypeScriptnpm install -D @types/react-router-dom
Configuração Base
1. Setup da aplicação:
// src/main.tsximport { 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.tsximport { 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.tsximport { Outlet, Link, useLocation } from 'react-router-dom'export const Layout = () => {const location = useLocation()return (<div className="layout"><nav className="navbar"><Linkto="/"className={location.pathname === '/' ? 'active' : ''}>Home</Link><Linkto="/about"className={location.pathname === '/about' ? 'active' : ''}>About</Link><Linkto="/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.tsximport { 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óricostate: { 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.tsximport { 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.tsximport { useSearchParams } from 'react-router-dom'import { useUsers } from '@/hooks/useUsers'export const Users = () => {const [searchParams, setSearchParams] = useSearchParams()// Ler parâmetros atuaisconst page = Number(searchParams.get('page')) || 1const 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><inputtype="text"value={search}onChange={(e) => updateSearch(e.target.value)}placeholder="Buscar usuários..."/><selectvalue={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 */}<PaginationcurrentPage={page}onPageChange={updatePage}/></div>)}
4. useLocation - Informações da localização:
// src/hooks/usePageTracking.tsimport { useEffect } from 'react'import { useLocation } from 'react-router-dom'export const usePageTracking = () => {const location = useLocation()useEffect(() => {// Analytics trackingwindow.gtag?.('config', 'GA_MEASUREMENT_ID', {page_path: location.pathname + location.search})// Log para debugconsole.log('Page changed:', {pathname: location.pathname,search: location.search,state: location.state})}, [location])}// src/components/Breadcrumbs.tsximport { 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.tsxexport 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.tsximport { Outlet, NavLink } from 'react-router-dom'export const DashboardLayout = () => {return (<div className="dashboard"><aside className="sidebar"><nav><NavLinkto="/dashboard"endclassName={({ isActive }) => isActive ? 'active' : ''}>Overview</NavLink><NavLinkto="/dashboard/analytics"className={({ isActive }) => isActive ? 'active' : ''}>Analytics</NavLink><NavLinkto="/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.tsximport { Navigate, useLocation } from 'react-router-dom'import { useAuth } from '@/hooks/useAuth'interface ProtectedRouteProps {children: React.ReactNoderoles?: string[]}export const ProtectedRoute = ({ children, roles }: ProtectedRouteProps) => {const { user, isAuthenticated } = useAuth()const location = useLocation()if (!isAuthenticated) {return (<Navigateto="/login"state={{ from: location }}replace/>)}if (roles && !roles.includes(user?.role)) {return (<Navigateto="/unauthorized"replace/>)}return <>{children}</>}
2. Uso em rotas:
// src/App.tsximport { 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 */}<Routepath="dashboard/*"element={<ProtectedRoute><DashboardRoutes /></ProtectedRoute>}/>{/* Rota apenas para admins */}<Routepath="admin/*"element={<ProtectedRoute roles={['admin']}><AdminRoutes /></ProtectedRoute>}/></Route></Routes>)}
Lazy Loading
1. Componentes lazy:
// src/App.tsximport { lazy, Suspense } from 'react'import { Routes, Route } from 'react-router-dom'// Lazy importsconst 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 />} /><Routepath="dashboard/*"element={<Suspense fallback={<div>Carregando Dashboard...</div>}><Dashboard /></Suspense>}/><Routepath="users/*"element={<Suspense fallback={<div>Carregando Users...</div>}><Users /></Suspense>}/><Routepath="analytics"element={<Suspense fallback={<div>Carregando Analytics...</div>}><Analytics /></Suspense>}/></Route></Routes>)}
2. Loading component reutilizável:
// src/components/RouteLoader.tsxinterface RouteLoaderProps {children: React.ReactNodefallback?: 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.tsximport { 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.tsximport { Component, ErrorInfo, ReactNode } from 'react'import { Link } from 'react-router-dom'interface Props {children: ReactNode}interface State {hasError: booleanerror?: 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.tsximport { 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.tsximport { 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 criadonavigate(`/users/${newUser.id}`, {state: { message: 'User created successfully' }})} catch (error) {alert('Error creating user')} finally {setLoading(false)}}return (<form onSubmit={handleSubmit}><inputtype="text"value={formData.name}onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}placeholder="Nome"required/><inputtype="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.tsximport { memo } from 'react'import { Route } from 'react-router-dom'interface OptimizedRouteProps {path: stringelement: React.ReactElement}export const OptimizedRoute = memo<OptimizedRouteProps>(({ path, element }) => (<Route path={path} element={element} />))
2. Navigation Guards:
// src/hooks/useNavigationGuard.tsimport { 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) returnconst 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.tsximport { 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()})})