Frontend
React
Biblioteca JavaScript para construção de interfaces de usuário com componentes reutilizáveis
O que é React?
React é uma biblioteca JavaScript desenvolvida pelo Facebook para construção de interfaces de usuário. Baseada em componentes reutilizáveis, oferece uma abordagem declarativa para o desenvolvimento de UIs interativas com gerenciamento de estado eficiente.
Por que utilizamos React na IngenioLab?
- Componentes reutilizáveis: Arquitetura modular e escalável
- Virtual DOM: Performance otimizada para atualizações de UI
- Ecossistema rico: Vasta gama de bibliotecas e ferramentas
- TypeScript friendly: Excelente suporte a tipagem estática
- Comunidade ativa: Grande suporte e recursos disponíveis
- Flexibilidade: Pode ser usado em projetos pequenos ou grandes
Instalação
Com Vite (Recomendado):
# Criar novo projeto React + TypeScriptnpm create vite@latest meu-projeto -- --template react-tscd meu-projetonpm installnpm run dev
Dependências essenciais:
# React corenpm install react react-dom# TypeScript supportnpm install -D @types/react @types/react-dom typescript# Development toolsnpm install -D @vitejs/plugin-react eslint-plugin-react
Conceitos Fundamentais
1. Componentes
Componentes Funcionais (Padrão IngenioLab):
// src/components/UserCard.tsximport { FC } from 'react'interface UserCardProps {id: stringname: stringemail: stringisActive?: booleanonEdit?: (id: string) => void}export const UserCard: FC<UserCardProps> = ({id,name,email,isActive = true,onEdit}) => {return (<div className="user-card"><h3>{name}</h3><p>{email}</p><span className={`status ${isActive ? 'active' : 'inactive'}`}>{isActive ? 'Ativo' : 'Inativo'}</span>{onEdit && (<button onClick={() => onEdit(id)}>Editar</button>)}</div>)}
Componentes com Children:
// src/components/Card.tsximport { FC, ReactNode } from 'react'interface CardProps {children: ReactNodetitle?: stringclassName?: string}export const Card: FC<CardProps> = ({ children, title, className = '' }) => {return (<div className={`card ${className}`}>{title && <h2 className="card-title">{title}</h2>}<div className="card-content">{children}</div></div>)}// Uso<Card title="Informações do Usuário"><UserCard id="1" name="João" email="joao@exemplo.com" /></Card>
2. State (useState)
Estado Local:
// src/components/Counter.tsximport { useState, FC } from 'react'export const Counter: FC = () => {const [count, setCount] = useState<number>(0)const [step, setStep] = useState<number>(1)const increment = () => setCount(prev => prev + step)const decrement = () => setCount(prev => prev - step)const reset = () => setCount(0)return (<div className="counter"><h2>Contador: {count}</h2><div className="controls"><label>Passo:<inputtype="number"value={step}onChange={(e) => setStep(Number(e.target.value))}min="1"/></label><button onClick={decrement}>-{step}</button><button onClick={reset}>Reset</button><button onClick={increment}>+{step}</button></div></div>)}
Estado Complexo:
// src/components/UserForm.tsximport { useState, FC } from 'react'interface User {name: stringemail: stringage: numberpreferences: {theme: 'light' | 'dark'notifications: booleanlanguage: 'pt' | 'en'}}export const UserForm: FC = () => {const [user, setUser] = useState<User>({name: '',email: '',age: 18,preferences: {theme: 'light',notifications: true,language: 'pt'}})// Atualizar campo simplesconst updateField = <K extends keyof User>(field: K,value: User[K]) => {setUser(prev => ({ ...prev, [field]: value }))}// Atualizar preferênciasconst updatePreference = <K extends keyof User['preferences']>(key: K,value: User['preferences'][K]) => {setUser(prev => ({...prev,preferences: {...prev.preferences,[key]: value}}))}const handleSubmit = (e: React.FormEvent) => {e.preventDefault()console.log('Dados do usuário:', user)}return (<form onSubmit={handleSubmit} className="user-form"><inputtype="text"placeholder="Nome"value={user.name}onChange={(e) => updateField('name', e.target.value)}/><inputtype="email"placeholder="Email"value={user.email}onChange={(e) => updateField('email', e.target.value)}/><inputtype="number"placeholder="Idade"value={user.age}onChange={(e) => updateField('age', Number(e.target.value))}/><selectvalue={user.preferences.theme}onChange={(e) => updatePreference('theme', e.target.value as 'light' | 'dark')}><option value="light">Claro</option><option value="dark">Escuro</option></select><label><inputtype="checkbox"checked={user.preferences.notifications}onChange={(e) => updatePreference('notifications', e.target.checked)}/>Receber notificações</label><button type="submit">Salvar</button></form>)}
3. Effects (useEffect)
Efeitos básicos:
// src/components/UserProfile.tsximport { useState, useEffect, FC } from 'react'interface User {id: stringname: stringemail: string}interface UserProfileProps {userId: string}export const UserProfile: FC<UserProfileProps> = ({ userId }) => {const [user, setUser] = useState<User | null>(null)const [loading, setLoading] = useState<boolean>(true)const [error, setError] = useState<string | null>(null)// Effect para buscar dados do usuáriouseEffect(() => {const fetchUser = async () => {try {setLoading(true)setError(null)const response = await fetch(`/api/users/${userId}`)if (!response.ok) {throw new Error('Usuário não encontrado')}const userData = await response.json()setUser(userData)} catch (err) {setError(err instanceof Error ? err.message : 'Erro desconhecido')} finally {setLoading(false)}}fetchUser()}, [userId]) // Dependência: re-executa quando userId mudar// Effect para cleanup (opcional)useEffect(() => {return () => {// Cleanup quando componente for desmontadoconsole.log('UserProfile desmontado')}}, [])if (loading) return <div>Carregando...</div>if (error) return <div>Erro: {error}</div>if (!user) return <div>Usuário não encontrado</div>return (<div className="user-profile"><h2>{user.name}</h2><p>{user.email}</p></div>)}
Effects avançados:
// src/hooks/useTimer.tsimport { useState, useEffect } from 'react'export const useTimer = (initialTime: number = 0, interval: number = 1000) => {const [time, setTime] = useState(initialTime)const [isRunning, setIsRunning] = useState(false)useEffect(() => {let intervalId: NodeJS.Timeout | null = nullif (isRunning) {intervalId = setInterval(() => {setTime(prevTime => prevTime + 1)}, interval)} else if (intervalId) {clearInterval(intervalId)}// Cleanup functionreturn () => {if (intervalId) {clearInterval(intervalId)}}}, [isRunning, interval])const start = () => setIsRunning(true)const pause = () => setIsRunning(false)const reset = () => {setTime(initialTime)setIsRunning(false)}return {time,isRunning,start,pause,reset}}// Uso do hookexport const TimerComponent: FC = () => {const { time, isRunning, start, pause, reset } = useTimer(0, 1000)return (<div><h2>Timer: {time}s</h2><button onClick={start} disabled={isRunning}>Start</button><button onClick={pause} disabled={!isRunning}>Pause</button><button onClick={reset}>Reset</button></div>)}
4. Context API
Criando Context:
// src/contexts/AuthContext.tsximport { createContext, useContext, useState, FC, ReactNode } from 'react'interface User {id: stringname: stringemail: stringrole: 'admin' | 'user'}interface AuthContextData {user: User | nullisAuthenticated: booleanlogin: (email: string, password: string) => Promise<void>logout: () => voidloading: boolean}const AuthContext = createContext<AuthContextData | undefined>(undefined)interface AuthProviderProps {children: ReactNode}export const AuthProvider: FC<AuthProviderProps> = ({ children }) => {const [user, setUser] = useState<User | null>(null)const [loading, setLoading] = useState<boolean>(false)const login = async (email: string, password: string) => {try {setLoading(true)const response = await fetch('/api/auth/login', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ email, password })})if (!response.ok) {throw new Error('Credenciais inválidas')}const { user, token } = await response.json()// Salvar token no localStoragelocalStorage.setItem('token', token)setUser(user)} catch (error) {throw error} finally {setLoading(false)}}const logout = () => {localStorage.removeItem('token')setUser(null)}const value = {user,isAuthenticated: !!user,login,logout,loading}return (<AuthContext.Provider value={value}>{children}</AuthContext.Provider>)}// Hook customizado para usar o contextexport const useAuth = () => {const context = useContext(AuthContext)if (context === undefined) {throw new Error('useAuth deve ser usado dentro de AuthProvider')}return context}
Usando Context:
// src/components/LoginForm.tsximport { useState, FC } from 'react'import { useAuth } from '../contexts/AuthContext'export const LoginForm: FC = () => {const { login, loading } = useAuth()const [email, setEmail] = useState('')const [password, setPassword] = useState('')const [error, setError] = useState<string | null>(null)const handleSubmit = async (e: React.FormEvent) => {e.preventDefault()try {setError(null)await login(email, password)} catch (err) {setError(err instanceof Error ? err.message : 'Erro no login')}}return (<form onSubmit={handleSubmit}>{error && <div className="error">{error}</div>}<inputtype="email"placeholder="Email"value={email}onChange={(e) => setEmail(e.target.value)}disabled={loading}/><inputtype="password"placeholder="Senha"value={password}onChange={(e) => setPassword(e.target.value)}disabled={loading}/><button type="submit" disabled={loading}>{loading ? 'Entrando...' : 'Entrar'}</button></form>)}// src/components/UserInfo.tsxexport const UserInfo: FC = () => {const { user, logout, isAuthenticated } = useAuth()if (!isAuthenticated) {return <div>Não logado</div>}return (<div className="user-info"><h3>Bem-vindo, {user?.name}!</h3><p>Email: {user?.email}</p><p>Role: {user?.role}</p><button onClick={logout}>Sair</button></div>)}
Hooks Customizados
1. Hook para Fetch de dados:
// src/hooks/useFetch.tsimport { useState, useEffect } from 'react'interface UseFetchOptions {method?: 'GET' | 'POST' | 'PUT' | 'DELETE'headers?: Record<string, string>body?: any}interface UseFetchReturn<T> {data: T | nullloading: booleanerror: string | nullrefetch: () => void}export function useFetch<T>(url: string,options: UseFetchOptions = {}): UseFetchReturn<T> {const [data, setData] = useState<T | null>(null)const [loading, setLoading] = useState<boolean>(true)const [error, setError] = useState<string | null>(null)const fetchData = async () => {try {setLoading(true)setError(null)const response = await fetch(url, {method: options.method || 'GET',headers: {'Content-Type': 'application/json',...options.headers},body: options.body ? JSON.stringify(options.body) : undefined})if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`)}const result = await response.json()setData(result)} catch (err) {setError(err instanceof Error ? err.message : 'Erro desconhecido')} finally {setLoading(false)}}useEffect(() => {fetchData()}, [url])return {data,loading,error,refetch: fetchData}}// Uso do hookexport const UserList: FC = () => {const { data: users, loading, error, refetch } = useFetch<User[]>('/api/users')if (loading) return <div>Carregando usuários...</div>if (error) return <div>Erro: {error}</div>return (<div><button onClick={refetch}>Recarregar</button>{users?.map(user => (<UserCard key={user.id} {...user} />))}</div>)}
2. Hook para LocalStorage:
// src/hooks/useLocalStorage.tsimport { useState, useEffect } from 'react'export function useLocalStorage<T>(key: string,initialValue: T): [T, (value: T | ((val: T) => T)) => void] {// Ler valor do localStorage na inicializaçãoconst [storedValue, setStoredValue] = useState<T>(() => {try {const item = window.localStorage.getItem(key)return item ? JSON.parse(item) : initialValue} catch (error) {console.error(`Error reading localStorage key "${key}":`, error)return initialValue}})// Função para atualizar o valorconst setValue = (value: T | ((val: T) => T)) => {try {// Permitir value ser uma função como no useStateconst valueToStore = value instanceof Function ? value(storedValue) : value// Salvar no statesetStoredValue(valueToStore)// Salvar no localStoragewindow.localStorage.setItem(key, JSON.stringify(valueToStore))} catch (error) {console.error(`Error setting localStorage key "${key}":`, error)}}return [storedValue, setValue]}// Uso do hookexport const ThemeToggle: FC = () => {const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')const toggleTheme = () => {setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light')}useEffect(() => {document.body.className = theme}, [theme])return (<button onClick={toggleTheme}>Tema atual: {theme}</button>)}
Estrutura de Projeto IngenioLab
src/├── components/ # Componentes reutilizáveis│ ├── ui/ # Componentes base (Button, Input, etc)│ ├── forms/ # Componentes de formulário│ └── layout/ # Componentes de layout├── contexts/ # React Contexts│ ├── AuthContext.tsx│ └── ThemeContext.tsx├── hooks/ # Hooks customizados│ ├── useAuth.ts│ ├── useFetch.ts│ └── useLocalStorage.ts├── pages/ # Páginas da aplicação├── services/ # Serviços de API├── types/ # Tipos TypeScript├── utils/ # Funções utilitárias└── App.tsx # Componente principal
Padrões de Performance
1. Memoização:
import { memo, useMemo, useCallback, FC } from 'react'interface ExpensiveComponentProps {data: any[]onItemClick: (id: string) => void}// Memoizar componente para evitar re-renders desnecessáriosexport const ExpensiveComponent: FC<ExpensiveComponentProps> = memo(({data,onItemClick}) => {// Memoizar cálculos pesadosconst processedData = useMemo(() => {return data.map(item => ({...item,calculated: item.value * 1.1 // Cálculo pesado}))}, [data])// Memoizar funções para evitar re-renders em childrenconst handleClick = useCallback((id: string) => {console.log('Item clicked:', id)onItemClick(id)}, [onItemClick])return (<div>{processedData.map(item => (<ItemComponentkey={item.id}item={item}onClick={handleClick}/>))}</div>)})
2. Lazy Loading:
import { lazy, Suspense, FC } from 'react'// Lazy load de componentesconst HeavyComponent = lazy(() => import('./HeavyComponent'))const UserDashboard = lazy(() => import('./UserDashboard'))export const App: FC = () => {return (<div><Suspense fallback={<div>Carregando...</div>}><HeavyComponent /></Suspense><Suspense fallback={<div>Carregando dashboard...</div>}><UserDashboard /></Suspense></div>)}
Testes com React
// src/components/__tests__/UserCard.test.tsximport { render, screen, fireEvent } from '@testing-library/react'import { UserCard } from '../UserCard'describe('UserCard', () => {const mockUser = {id: '1',name: 'João Silva',email: 'joao@exemplo.com'}it('should render user information', () => {render(<UserCard {...mockUser} />)expect(screen.getByText('João Silva')).toBeInTheDocument()expect(screen.getByText('joao@exemplo.com')).toBeInTheDocument()})it('should call onEdit when edit button is clicked', () => {const mockOnEdit = vi.fn()render(<UserCard {...mockUser} onEdit={mockOnEdit} />)fireEvent.click(screen.getByText('Editar'))expect(mockOnEdit).toHaveBeenCalledWith('1')})})