Arquitetura software
Ecossistema React
React e a biblioteca mais popular para construcao de interfaces de usuario. Este guia cobre o ecossistema completo para desenvolvimento web e mobile.
React para Web
Frameworks
| Framework | Tipo | Caracteristica Principal |
|---|---|---|
| Next.js | Full-stack | SSR, SSG, App Router |
| Vite + React | SPA | Build ultra-rapido |
| Remix | Full-stack | Web standards, nested routes |
| Gatsby | Static | CMS, sites estaticos |
Next.js
Framework React mais popular para producao.
// app/page.tsx (App Router)
export default function Home() {
return (
<main>
<h1>Bem-vindo</h1>
</main>
)
}
// app/users/[id]/page.tsx
interface Props {
params: { id: string }
}
export default async function UserPage({ params }: Props) {
const user = await fetch(`/api/users/${params.id}`).then(r => r.json())
return <div>{user.name}</div>
}Server Components vs Client Components
// Server Component (padrao)
// Executa no servidor, sem JavaScript no cliente
async function ServerComponent() {
const data = await db.query('SELECT * FROM users')
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
// Client Component
'use client'
import { useState } from 'react'
function ClientComponent() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}Server Actions
// app/actions.ts
'use server'
export async function createUser(formData: FormData) {
const name = formData.get('name')
await db.insert({ name })
revalidatePath('/users')
}
// app/users/new/page.tsx
import { createUser } from '../actions'
export default function NewUser() {
return (
<form action={createUser}>
<input name="name" />
<button type="submit">Criar</button>
</form>
)
}React Native e Expo
Expo
Framework para desenvolvimento mobile com React Native.
# Criar projeto
npx create-expo-app@latest meu-app
# Rodar
npx expo start// app/(tabs)/index.tsx (Expo Router)
import { View, Text, StyleSheet } from 'react-native'
export default function HomeScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Meu App</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
})Expo Router
Navegacao baseada em arquivos (similar ao Next.js).
app/
├── (tabs)/
│ ├── _layout.tsx # Tab Navigator
│ ├── index.tsx # Tab 1
│ └── settings.tsx # Tab 2
├── user/
│ └── [id].tsx # Rota dinamica
└── _layout.tsx # Layout raiz// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router'
import { Ionicons } from '@expo/vector-icons'
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color }) => (
<Ionicons name="home" size={24} color={color} />
),
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'Config',
tabBarIcon: ({ color }) => (
<Ionicons name="settings" size={24} color={color} />
),
}}
/>
</Tabs>
)
}Gerenciamento de Estado
Zustand
Estado global simples e performatico.
import { create } from 'zustand'
interface UserStore {
user: User | null
setUser: (user: User) => void
logout: () => void
}
const useUserStore = create<UserStore>((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
}))
// Uso
function Profile() {
const { user, logout } = useUserStore()
return (
<div>
<p>{user?.name}</p>
<button onClick={logout}>Sair</button>
</div>
)
}TanStack Query (React Query)
Gerenciamento de estado de servidor.
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
function UserList() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
})
if (isLoading) return <p>Carregando...</p>
if (error) return <p>Erro: {error.message}</p>
return (
<ul>
{data.map((user: User) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
function CreateUser() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (newUser: NewUser) =>
fetch('/api/users', {
method: 'POST',
body: JSON.stringify(newUser),
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
return (
<button onClick={() => mutation.mutate({ name: 'Novo User' })}>
Criar
</button>
)
}Estilizacao
Tailwind CSS
function Card({ title, description }: CardProps) {
return (
<div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
<h2 className="text-xl font-bold text-gray-800 mb-2">{title}</h2>
<p className="text-gray-600">{description}</p>
</div>
)
}NativeWind (Tailwind para React Native)
import { View, Text } from 'react-native'
function Card({ title }: { title: string }) {
return (
<View className="bg-white rounded-lg p-4 shadow-md">
<Text className="text-xl font-bold text-gray-800">{title}</Text>
</View>
)
}Styled Components
import styled from 'styled-components'
const Card = styled.div`
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
`
const Title = styled.h2`
color: #1a1a1a;
font-size: 1.25rem;
margin-bottom: 8px;
`Formularios
React Hook Form + Zod
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const schema = z.object({
email: z.string().email('Email invalido'),
password: z.string().min(8, 'Minimo 8 caracteres'),
})
type FormData = z.infer<typeof schema>
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({
resolver: zodResolver(schema),
})
const onSubmit = async (data: FormData) => {
await login(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} placeholder="Email" />
{errors.email && <span>{errors.email.message}</span>}
<input {...register('password')} type="password" placeholder="Senha" />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Entrando...' : 'Entrar'}
</button>
</form>
)
}Testes
Vitest + Testing Library
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { Counter } from './Counter'
describe('Counter', () => {
it('renders initial count', () => {
render(<Counter initialCount={5} />)
expect(screen.getByText('Count: 5')).toBeInTheDocument()
})
it('increments count on click', async () => {
render(<Counter initialCount={0} />)
fireEvent.click(screen.getByRole('button', { name: /increment/i }))
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})
})React Native Testing Library
import { render, fireEvent } from '@testing-library/react-native'
import { LoginScreen } from './LoginScreen'
describe('LoginScreen', () => {
it('shows error for invalid email', () => {
const { getByPlaceholderText, getByText } = render(<LoginScreen />)
fireEvent.changeText(getByPlaceholderText('Email'), 'invalid')
fireEvent.press(getByText('Entrar'))
expect(getByText('Email invalido')).toBeTruthy()
})
})Estrutura de Projeto
Next.js (App Router)
src/
├── app/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ └── register/page.tsx
│ ├── (dashboard)/
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── api/
│ │ └── users/route.ts
│ ├── layout.tsx
│ └── page.tsx
├── components/
│ ├── ui/
│ │ ├── Button.tsx
│ │ └── Input.tsx
│ └── features/
│ └── UserCard.tsx
├── lib/
│ ├── api.ts
│ └── utils.ts
├── hooks/
│ └── useUser.ts
└── types/
└── user.tsExpo
app/
├── (tabs)/
│ ├── _layout.tsx
│ ├── index.tsx
│ └── profile.tsx
├── user/[id].tsx
├── _layout.tsx
└── +not-found.tsx
src/
├── components/
├── hooks/
├── services/
├── stores/
└── types/