2.4 — Gerenciamento de Estado: Stores vs Context/Redux
Como compartilhar estado entre componentes sem enlouquecer.
Objetivos da Aula
- Entender o problema de estado global
- Comparar Context API, Redux/Zustand com Svelte Stores
- Ver como Svelte simplifica drasticamente o gerenciamento de estado
O Problema: Prop Drilling
Prop Drilling
App tem o estado
user└── Layout passa
user ↓└── Sidebar passa
user ↓└── UserMenu passa
user ↓└── Avatar finalmente usa
user! ✓4 componentes intermediarios so passando props!
Problema: 4 componentes intermediarios so passando props!
React: Context API
Criando um Context
// contexts/UserContext.jsx
import { createContext, useContext, useState } from 'react'
// Criar o contexto
const UserContext = createContext(null)
// Provider component
export function UserProvider({ children }) {
const [user, setUser] = useState(null)
const login = async (credentials) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
const userData = await response.json()
setUser(userData)
}
const logout = () => setUser(null)
// Valor do contexto
const value = {
user,
login,
logout,
isAuthenticated: !!user
}
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
)
}
// Hook customizado para consumir
export function useUser() {
const context = useContext(UserContext)
if (!context) {
throw new Error('useUser must be used within UserProvider')
}
return context
} Usando o Context
// App.jsx
import { UserProvider } from './contexts/UserContext'
function App() {
return (
<UserProvider>
<Layout />
</UserProvider>
)
}
// Avatar.jsx (componente profundamente aninhado)
import { useUser } from './contexts/UserContext'
function Avatar() {
const { user, logout } = useUser()
return (
<div>
<img src={user?.avatar} alt={user?.name} />
<button onClick={logout}>Sair</button>
</div>
)
} Problemas do Context
// ❌ Problema 1: Re-renders desnecessários
// Qualquer mudança no contexto re-renderiza TODOS os consumidores
const value = {
user, // Mudou user?
settings, // Todos que usam settings também re-renderizam!
theme, // Todos que usam theme também!
}
// ❌ Problema 2: Múltiplos providers aninhados (Provider Hell)
function App() {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>
<NotificationProvider>
<ToastProvider>
<ModalProvider>
<Layout />
</ModalProvider>
</ToastProvider>
</NotificationProvider>
</CartProvider>
</ThemeProvider>
</AuthProvider>
)
} React: Redux/Zustand
Redux (verboso)
// store/userSlice.js
import { createSlice } from '@reduxjs/toolkit'
const userSlice = createSlice({
name: 'user',
initialState: { data: null, loading: false, error: null },
reducers: {
loginStart: (state) => {
state.loading = true
},
loginSuccess: (state, action) => {
state.data = action.payload
state.loading = false
},
loginError: (state, action) => {
state.error = action.payload
state.loading = false
},
logout: (state) => {
state.data = null
}
}
})
export const { loginStart, loginSuccess, loginError, logout } = userSlice.actions
export default userSlice.reducer
// store/index.js
import { configureStore } from '@reduxjs/toolkit'
import userReducer from './userSlice'
export const store = configureStore({
reducer: {
user: userReducer
}
})
// Componente
import { useSelector, useDispatch } from 'react-redux'
import { loginSuccess, logout } from './store/userSlice'
function Avatar() {
const user = useSelector(state => state.user.data)
const dispatch = useDispatch()
return (
<div>
<img src={user?.avatar} />
<button onClick={() => dispatch(logout())}>Sair</button>
</div>
)
} Zustand (mais simples)
// stores/userStore.js
import { create } from 'zustand'
const useUserStore = create((set) => ({
user: null,
loading: false,
login: async (credentials) => {
set({ loading: true })
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
const user = await response.json()
set({ user, loading: false })
},
logout: () => set({ user: null })
}))
export default useUserStore
// Componente
import useUserStore from './stores/userStore'
function Avatar() {
const user = useUserStore(state => state.user)
const logout = useUserStore(state => state.logout)
return (
<div>
<img src={user?.avatar} />
<button onClick={logout}>Sair</button>
</div>
)
} Svelte: Stores Nativos
Stores Built-in
// stores/user.js
import { writable } from 'svelte/store'
// Criar store (muito simples!)
export const user = writable(null)
// Métodos customizados
function createUserStore() {
const { subscribe, set, update } = writable(null)
return {
subscribe, // Obrigatório para ser um store
login: async (credentials) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
const userData = await response.json()
set(userData)
},
logout: () => set(null),
updateProfile: (data) => update(user => ({ ...user, ...data }))
}
}
export const userStore = createUserStore() Usando Stores em Componentes
<!-- Avatar.svelte -->
<script>
import { userStore } from './stores/user.js'
// $ prefix = auto-subscribe! ✨
// Sem boilerplate, sem hooks, sem providers
</script>
<div>
<img src={$userStore?.avatar} alt={$userStore?.name} />
<button on:click={userStore.logout}>Sair</button>
</div> Isso é tudo. Sem providers, sem hooks, sem boilerplate.
Comparação de Boilerplate
React Context: ~50 linhas
// context + provider + hook + uso = ~50 linhas
const UserContext = createContext(null)
export function UserProvider({ children }) {
const [user, setUser] = useState(null)
const login = async (cred) => { /* ... */ }
const logout = () => setUser(null)
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
)
}
export function useUser() {
const ctx = useContext(UserContext)
if (!ctx) throw new Error('...')
return ctx
}
// App.jsx
<UserProvider><App /></UserProvider>
// Component.jsx
const { user, logout } = useUser() Svelte Store: ~15 linhas
// stores/user.js
import { writable } from 'svelte/store'
function createUserStore() {
const { subscribe, set } = writable(null)
return {
subscribe,
login: async (cred) => { /* ... */ set(user) },
logout: () => set(null)
}
}
export const userStore = createUserStore() <!-- Component.svelte -->
<script>
import { userStore } from './stores/user'
</script>
<span>{$userStore?.name}</span> Tipos de Stores
writable — Estado mutável
import { writable } from 'svelte/store'
// Simples
const count = writable(0)
count.set(5) // Define valor
count.update(n => n + 1) // Atualiza baseado no atual
// Com valor inicial complexo
const settings = writable({
theme: 'dark',
language: 'pt-BR',
notifications: true
}) readable — Estado somente leitura
import { readable } from 'svelte/store'
// Hora atual (atualiza a cada segundo)
const time = readable(new Date(), (set) => {
const interval = setInterval(() => {
set(new Date())
}, 1000)
// Cleanup quando último subscriber sair
return () => clearInterval(interval)
})
// Posição do mouse
const mousePosition = readable({ x: 0, y: 0 }, (set) => {
const handler = (e) => set({ x: e.clientX, y: e.clientY })
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}) derived — Valores computados
import { writable, derived } from 'svelte/store'
const items = writable([
{ name: 'Maçã', price: 2, qty: 3 },
{ name: 'Banana', price: 1, qty: 5 }
])
// Derivado de um store
const total = derived(items, $items =>
$items.reduce((sum, item) => sum + item.price * item.qty, 0)
)
// Derivado de múltiplos stores
const user = writable({ name: 'João' })
const greeting = derived([user, time], ([$user, $time]) =>
`Olá ${$user.name}! São ${$time.toLocaleTimeString()}`
) Equivalências React → Svelte
| React | Svelte |
|---|---|
useState | let ou writable |
useContext | $store (auto-subscribe) |
useReducer | writable + update function |
useMemo com deps | derived store |
| Redux store | writable + custom methods |
| Zustand | writable + custom methods |
| Jotai atoms | writable stores |
| Recoil atoms | writable stores |
Store Avançado: Carrinho de Compras
React + Context
// CartContext.jsx
const CartContext = createContext()
export function CartProvider({ children }) {
const [items, setItems] = useState([])
const addItem = (product) => {
setItems(prev => {
const existing = prev.find(i => i.id === product.id)
if (existing) {
return prev.map(i =>
i.id === product.id
? { ...i, qty: i.qty + 1 }
: i
)
}
return [...prev, { ...product, qty: 1 }]
})
}
const removeItem = (id) => {
setItems(prev => prev.filter(i => i.id !== id))
}
const updateQty = (id, qty) => {
if (qty <= 0) {
removeItem(id)
return
}
setItems(prev => prev.map(i =>
i.id === id ? { ...i, qty } : i
))
}
const clear = () => setItems([])
const total = useMemo(() =>
items.reduce((sum, i) => sum + i.price * i.qty, 0),
[items]
)
const itemCount = useMemo(() =>
items.reduce((sum, i) => sum + i.qty, 0),
[items]
)
return (
<CartContext.Provider value={{
items, addItem, removeItem, updateQty, clear, total, itemCount
}}>
{children}
</CartContext.Provider>
)
}
export const useCart = () => useContext(CartContext) Svelte Store
// stores/cart.js
import { writable, derived } from 'svelte/store'
function createCartStore() {
const { subscribe, set, update } = writable([])
return {
subscribe,
addItem: (product) => update(items => {
const existing = items.find(i => i.id === product.id)
if (existing) {
return items.map(i =>
i.id === product.id
? { ...i, qty: i.qty + 1 }
: i
)
}
return [...items, { ...product, qty: 1 }]
}),
removeItem: (id) => update(items =>
items.filter(i => i.id !== id)
),
updateQty: (id, qty) => update(items => {
if (qty <= 0) return items.filter(i => i.id !== id)
return items.map(i => i.id === id ? { ...i, qty } : i)
}),
clear: () => set([])
}
}
export const cart = createCartStore()
// Stores derivados (calculados automaticamente)
export const cartTotal = derived(cart, $cart =>
$cart.reduce((sum, i) => sum + i.price * i.qty, 0)
)
export const cartCount = derived(cart, $cart =>
$cart.reduce((sum, i) => sum + i.qty, 0)
) Uso
<!-- CartIcon.svelte -->
<script>
import { cartCount } from './stores/cart'
</script>
<div class="cart-icon">
🛒
{#if $cartCount > 0}
<span class="badge">{$cartCount}</span>
{/if}
</div>
<!-- CartSummary.svelte -->
<script>
import { cart, cartTotal } from './stores/cart'
</script>
<div class="cart">
{#each $cart as item (item.id)}
<div class="item">
<span>{item.name}</span>
<span>x{item.qty}</span>
<button on:click={() => cart.removeItem(item.id)}>×</button>
</div>
{/each}
<div class="total">
Total: R$ {$cartTotal.toFixed(2)}
</div>
<button on:click={cart.clear}>Limpar</button>
</div> Persistência em localStorage
Svelte (simples!)
// stores/persisted.js
import { writable } from 'svelte/store'
function createPersistedStore(key, initialValue) {
// Tenta carregar do localStorage
const stored = localStorage.getItem(key)
const initial = stored ? JSON.parse(stored) : initialValue
const store = writable(initial)
// Salva no localStorage quando mudar
store.subscribe(value => {
localStorage.setItem(key, JSON.stringify(value))
})
return store
}
// Uso
export const settings = createPersistedStore('settings', {
theme: 'light',
language: 'pt-BR'
})
export const cart = createPersistedStore('cart', []) ✅ Desafio da Aula
Objetivo
Criar um sistema de temas (dark/light) com Svelte stores.
Requisitos
- Store
themeque guarda ‘light’ ou ‘dark’ - Store derivado
isDarkque retorna boolean - Função
toggle()para alternar - Persistir no localStorage
- Componente
ThemeToggleque usa o store
Spec de Verificação
- Tema inicial carrega do localStorage (ou ‘light’ se não existir)
- Clicar no toggle alterna entre light/dark
- Recarregar página mantém o tema escolhido
-
$isDarkreflete corretamente o tema atual
Solução
🔍 Clique para ver a solução
// stores/theme.js
import { writable, derived } from 'svelte/store'
import { browser } from '$app/environment' // SvelteKit
// ou: const browser = typeof window !== 'undefined'
function createThemeStore() {
// Valor inicial do localStorage ou 'light'
const initial = browser
? localStorage.getItem('theme') || 'light'
: 'light'
const { subscribe, set, update } = writable(initial)
// Persiste mudanças
if (browser) {
subscribe(value => {
localStorage.setItem('theme', value)
// Opcional: aplica classe no body
document.body.classList.toggle('dark', value === 'dark')
})
}
return {
subscribe,
set,
toggle: () => update(t => t === 'light' ? 'dark' : 'light'),
setLight: () => set('light'),
setDark: () => set('dark')
}
}
export const theme = createThemeStore()
export const isDark = derived(theme, $theme => $theme === 'dark') <!-- ThemeToggle.svelte -->
<script>
import { theme, isDark } from './stores/theme'
</script>
<button
on:click={theme.toggle}
class="theme-toggle"
aria-label="Alternar tema"
>
{$isDark ? '🌙' : '☀️'}
</button>
<style>
.theme-toggle {
font-size: 1.5rem;
background: none;
border: none;
cursor: pointer;
}
</style>🧪 Exercício Interativo
Pratique o que aprendeu com o exercício interativo!
📁 Local: exercicios/modulo-02/exercicio-2.4/
cd ../../exercicios/modulo-02/exercicio-2.4
npm install
npm test No exercício você vai implementar:
createThemeStore()- Store de tema com subscribe, set e togglederived()- Função para criar stores derivados
Todos os testes passando = aula concluída com sucesso! ✅
Próxima aula: 2.5 — Estilização: CSS com Escopo vs CSS-in-JS