Svelte 5 Runes: Guia Definitivo para Estado Global em Produção
Runes tornam a reatividade explícita ($state), funcionam em qualquer arquivo .ts, e eliminam a necessidade de stores na maioria dos casos. É como ter useState do React, mas mais poderoso
Ouça este artigo
Svelte 5 Runes: Guia Definitivo para Estado Global em Produção
Introdução
Já se viu refatorando um componente Svelte simples para um complexo sistema de stores (writable, readable) apenas para compartilhar um único estado reativo, como um tema de UI ou o status de autenticação do usuário? Essa transição, embora funcional, sempre pareceu um salto de complexidade. O modelo de reatividade implícito do Svelte, poderoso dentro de um componente, exigia uma API totalmente diferente para o estado global, criando uma dissonância cognitiva e, por vezes, um emaranhado de assinaturas e desinscrições manuais.
Essa era é passado. Com o Svelte 5, que entrou em Release Candidate em meados de 2024, a introdução das Runes representa a mudança mais fundamental no framework desde sua criação. Não se trata de uma mera adição de sintaxe; é uma reimaginação completa do motor de reatividade, que passa de implícito e baseado em componentes para explícito, granular e universal. Para engenheiros experientes, isso é crucial: significa previsibilidade, performance otimizada e, o mais importante, um modelo unificado para gerenciar estado local e global.
Neste artigo, faremos um mergulho técnico profundo nas Svelte 5 Runes. Vamos dissecar como elas funcionam sob o capô, por que eliminam a necessidade de stores tradicionais em muitos cenários, e como implementar um gerenciamento de estado global robusto e "production-ready" em um projeto SvelteKit moderno. Você aprenderá não apenas a sintaxe, mas os padrões de design, as armadilhas de produção e os trade-offs que vêm com esse novo poder.
O que mudou? O que são as Runes?
Para entender a magnitude da mudança, recordemos o Svelte "clássico" (versão 4 e anteriores). A reatividade era "mágica", um truque do compilador.
let count = 0;: Uma variável de estado local.$: doubled = count * 2;: Uma declaração reativa (um "efeito colateral" ou valor derivado).count++: Uma atribuição que o compilador intercepta para invalidar o componente e agendar uma nova renderização.
Esse modelo era incrivelmente simples para iniciantes, mas tinha limitações significativas para sistemas complexos:
- Reatividade Confinada: A "mágica" só funcionava dentro de arquivos
.svelte. Para compartilhar estado, era necessário usar a API de Stores (writable,readable,derived), que tinha uma ergonomia diferente (exigindosubscribe,updateou o prefixo$). - Invalidação em Nível de Componente: Uma atribuição reativa marcava o componente inteiro como "sujo", mesmo que apenas um pequeno trecho do DOM precisasse de atualização. Embora o Svelte fosse eficiente em aplicar as mudanças no DOM, o trabalho de reconciliação era em nível de componente.
- Falta de Clareza: Onde a reatividade começava e terminava? Para um engenheiro vindo de React ou Vue, a ausência de chamadas explícitas como
useStateourefpodia ser desconcertante e levar a comportamentos inesperados.
As Runes, introduzidas oficialmente com o Svelte 5 (atualmente em svelte@5.0.0-rc.1), resolvem esses problemas ao tornar a reatividade explícita e granular. Elas são sinais (signals) em sua essência, um padrão que ganhou imensa tração em frameworks como Solid.js e Preact.
As Runes são funções especiais, prefixadas com $, que o compilador do Svelte entende e otimiza:
$state: Declara um valor de estado reativo. É o equivalente aletno Svelte 4, mas explícito.$derived: Cria um valor computado que reage a mudanças em outros estados. Substitui$:.$effect: Executa um efeito colateral que reage a mudanças de estado. Também substitui$:, mas para operações (ex: logging, chamadas de API) em vez de computações de valor.
A razão pela qual a comunidade de desenvolvimento web está em polvorosa é que essa mudança, embora adicionando um pouco de verbosidade, unifica o modelo mental. A mesma reatividade que você usa para um contador local agora pode ser exportada de um arquivo .ts e usada para gerenciar o estado global de toda a aplicação, sem stores. Isso elimina a fronteira artificial entre estado local e global, um dos maiores atrativos do Svelte 5.
Aspectos Técnicos
A mudança para Runes é uma transição de um sistema de "dirty-checking" em nível de componente para um modelo de reatividade fina baseado em sinais. Cada Rune cria um "sinal" – um objeto que contém um valor e mantém uma lista de assinantes (outros sinais derivados ou efeitos).
Quando um $state é modificado, ele notifica apenas os $derived e $effect que o leem diretamente. O compilador do Svelte 5 então gera um código JavaScript otimizado que atualiza precisamente o nó do DOM ou executa o efeito colateral necessário, sem reavaliar o componente inteiro.
Arquitetura do Estado Global com Runes
O padrão de design para estado global com Runes é drasticamente simplificado. Em vez de criar instâncias de writable, agora exportamos diretamente os sinais de um módulo TypeScript.
Comparativo: Estado Global antes e depois das Runes
Versão de dependências para os exemplos:
{
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"svelte": "^5.0.0-rc.1",
"typescript": "^5.0.0",
"vite": "^5.0.0"
}
}Antes (Svelte 4, com Stores):
// src/lib/stores/cart.ts
import { writable, derived } from 'svelte/store';
export const items = writable<{ id: number, name: string, price: number }[]>([]);
export function addItem(item: { id: number, name: string, price: number }) {
items.update(currentItems => [...currentItems, item]);
}
export const itemCount = derived(items, $items => $items.length);
export const totalPrice = derived(items, $items =>
$items.reduce((sum, item) => sum + item.price, 0)
);Para usar no componente:
<!-- src/components/Cart.svelte -->
<script lang="ts">
import { itemCount, totalPrice } from '$lib/stores/cart';
</script>
<p>Itens: {$itemCount}</p>
<p>Total: R$ {$totalPrice.toFixed(2)}</p>Note a necessidade de writable, derived, a função addItem com update, e o uso do prefixo $ para auto-subscrição nos componentes.
Agora (Svelte 5, com Runes):
// src/lib/state/cart.ts
import { browser } from '$app/environment';
// Tipagem para os itens do carrinho
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
// Estado inicial carregado do localStorage, se disponível
function getInitialItems(): CartItem[] {
if (!browser) return []; // Não executar no SSR
try {
const stored = localStorage.getItem('cartItems');
return stored ? JSON.parse(stored) : [];
} catch (e) {
console.error("Failed to parse cart items from localStorage", e);
return [];
}
}
// Usando $state para o estado principal do carrinho.
// Ele agora é a fonte da verdade reativa.
export const items = $state<CartItem[]>(getInitialItems());
// Valores derivados que reagem automaticamente a mudanças em `items`
export const itemCount = $derived(
items.reduce((sum, item) => sum + item.quantity, 0)
);
export const totalPrice = $derived(
items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
);
// Ações para modificar o estado. Elas manipulam o $state diretamente.
export function addItem(product: Omit<CartItem, 'quantity'>) {
const existingItem = items.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity += 1;
} else {
// A mutação direta do array (push) funciona com $state
items.push({ ...product, quantity: 1 });
}
}
export function removeItem(productId: number) {
const itemIndex = items.findIndex(item => item.id === productId);
if (itemIndex > -1) {
// Splicing também é uma mutação que aciona a reatividade
items.splice(itemIndex, 1);
}
}
// Um efeito para persistir o estado no localStorage a cada mudança
$effect(() => {
if (browser) {
try {
localStorage.setItem('cartItems', JSON.stringify(items));
} catch (e) {
console.error("Failed to save cart items to localStorage", e);
}
}
});
A sintaxe é mais limpa e o modelo mental, unificado. items é um estado reativo, ponto. itemCount e totalPrice são valores derivados. As funções addItem e removeItem simplesmente manipulam o array items diretamente. O $effect sincroniza com o localStorage de forma transparente. Não há mais update ou subscribe. A mesma lógica se aplica dentro ou fora de um componente.
Breaking Changes e Migração:
Svelte 5 pode ser usado em um "modo sem runes" para compatibilidade, mas o futuro é com elas habilitadas. A migração envolve:
- Habilitar runes na configuração (
svelte.config.js). Em projetos novos comsvelte@nextjá vem por padrão. - Substituir
letpor$state()para estado reativo. - Substituir
$: labelpor$derived()para valores computados e$effect()para efeitos colaterais. - Substituir
export letpor$props()para declarar as props do componente.
Na Prática
Vamos construir um caso de uso real: um sistema de carrinho de compras compartilhado entre uma página de produtos e uma barra de navegação, usando SvelteKit.
Setup do Projeto:
# Certifique-se de estar usando Node.js 18+
npm create svelte@next my-svelte5-app
cd my-svelte5-app
# Siga os prompts (SvelteKit, TypeScript, etc.)
npm install
npm run dev1. Definir o Estado Global (src/lib/state/cart.ts)
Usaremos o código que acabamos de discutir, que já inclui persistência no localStorage.
// src/lib/state/cart.ts
// (Cole o código da seção anterior aqui)
import { browser } from '$app/environment';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
function getInitialItems(): CartItem[] {
if (!browser) return [];
try {
const stored = localStorage.getItem('cartItems');
return stored ? JSON.parse(stored) : [];
} catch (e) {
console.error("Failed to parse cart items from localStorage", e);
return [];
}
}
export const items = $state<CartItem[]>(getInitialItems());
export const itemCount = $derived(items.reduce((sum, item) => sum + item.quantity, 0));
export const totalPrice = $derived(items.reduce((sum, item) => sum + (item.price * item.quantity), 0));
export function addItem(product: Omit<CartItem, 'quantity'>) {
const existingItem = items.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity += 1;
} else {
items.push({ ...product, quantity: 1 });
}
}
export function removeItem(productId: number) {
const itemIndex = items.findIndex(item => item.id === productId);
if (itemIndex > -1) {
if (items[itemIndex].quantity > 1) {
items[itemIndex].quantity -= 1;
} else {
items.splice(itemIndex, 1);
}
}
}
$effect(() => {
if (browser) {
try {
localStorage.setItem('cartItems', JSON.stringify(items));
} catch (e) {
console.error("Failed to save cart items to localStorage", e);
}
}
});2. Criar a Lista de Produtos (src/routes/+page.svelte)
Esta página exibirá produtos e permitirá adicioná-los ao carrinho.
<script lang="ts">
import { addItem } from '$lib/state/cart';
import CartStatus from '$lib/components/CartStatus.svelte';
// Dados mockados de produtos
const products = [
{ id: 1, name: 'Svelte 5 T-Shirt', price: 25.0 },
{ id: 2, name: 'Rune-Powered Mug', price: 15.5 },
{ id: 3, name: 'SvelteKit Cap', price: 18.0 }
];
function handleAddToCart(product: (typeof products)[0]) {
// Simplesmente chama a função de ação do nosso estado global
addItem(product);
console.log(`Added ${product.name} to cart.`);
}
</script>
<svelte:head>
<title>Svelte 5 Shop</title>
</svelte:head>
<main>
<h1>Loja Svelte 5 com Runes</h1>
<CartStatus />
<div class="product-grid">
{#each products as product}
<div class="product-card">
<h2>{product.name}</h2>
<p>Preço: R$ {product.price.toFixed(2)}</p>
<button on:click={() => handleAddToCart(product)}>
Adicionar ao Carrinho
</button>
</div>
{/each}
</div>
</main>
<style>
main {
max-width: 800px;
margin: 2rem auto;
font-family: sans-serif;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
margin-top: 2rem;
}
.product-card {
border: 1px solid #ccc;
padding: 1rem;
border-radius: 8px;
text-align: center;
}
button {
padding: 0.5rem 1rem;
font-size: 1rem;
cursor: pointer;
}
</style>3. Criar o Componente de Status do Carrinho (src/lib/components/CartStatus.svelte)
Este componente pode ser usado em qualquer lugar (na página principal, no layout, etc.) para exibir o estado atual do carrinho.
<script lang="ts">
// Importamos os valores derivados diretamente. Sem '$:' ou 'subscribe'
import { itemCount, totalPrice, items, removeItem } from '$lib/state/cart';
let showDetails = $state(false);
</script>
<div class="cart-status">
<h3>
Carrinho: {itemCount} {itemCount === 1 ? 'item' : 'itens'} - Total: R$ {totalPrice.toFixed(2)}
</h3>
{#if itemCount > 0}
<button on:click={() => showDetails = !showDetails}>
{showDetails ? 'Esconder' : 'Mostrar'} Detalhes
</button>
{/if}
{#if showDetails && items.length > 0}
<ul>
{#each items as item (item.id)}
<li>
{item.name} (x{item.quantity}) - R$ {(item.price * item.quantity).toFixed(2)}
<button class="remove-btn" on:click={() => removeItem(item.id)}>Remover 1</button>
</li>
{/each}
</ul>
{/if}
</div>
<style>
.cart-status {
border: 2px solid #ff3e00;
padding: 1rem;
border-radius: 8px;
background-color: #fff5f0;
}
ul {
list-style: none;
padding: 0;
margin-top: 1rem;
}
li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #eee;
}
.remove-btn {
background-color: #ffcccc;
border: 1px solid red;
font-size: 0.8em;
padding: 0.2rem 0.4rem;
}
</style>Este exemplo completo demonstra a beleza das Runes: o estado (cart.ts) é um módulo JavaScript puro. Os componentes (+page.svelte, CartStatus.svelte) são consumidores reativos desse estado, sem qualquer acoplamento além da importação das variáveis e funções. A reatividade simplesmente funciona, de forma granular e eficiente.
Production Concerns
Adotar uma nova tecnologia em produção requer mais do que apenas entender a API principal. Aqui estão as considerações críticas para usar Svelte 5 Runes em um ambiente de produção.
-
Security: O estado reativo não é inerentemente seguro. Se você está preenchendo um
$statecom dados de uma API ou input do usuário, a validação é sua responsabilidade. Use bibliotecas como Zod para validar a estrutura dos dados antes de atribuí-los ao estado, prevenindo a poluição do estado com dados malformados.import { z } from 'zod'; const userSchema = z.object({ id: z.string(), name: z.string().min(2) }); let user = $state<{ id: string; name: string } | null>(null); function setUserFromApi(apiData: unknown) { const result = userSchema.safeParse(apiData); if (result.success) { user = result.data; } else { // Log do erro de validação console.error("Invalid user data from API:", result.error); } } -
Error Handling: E se um valor derivado depende de uma operação que pode falhar?
$derivednão tem um mecanismo detry/catchembutido. O padrão é gerenciar o estado de erro separadamente. Para dados assíncronos, use um padrão de "recurso" que encapsula os estados deloading,erroredata.import { fetchUserData } from './api'; let userId = $state<string | null>('1'); let userResource = $state({ isLoading: true, data: null as User | null, error: null as Error | null }); $effect(() => { if (!userId) return; userResource.isLoading = true; userResource.error = null; const controller = new AbortController(); fetchUserData(userId, controller.signal) .then(data => userResource.data = data) .catch(err => { if (err.name !== 'AbortError') { userResource.error = err; } }) .finally(() => userResource.isLoading = false); // Cleanup function for the effect return () => { controller.abort(); }; }); // Em seu template: // {#if userResource.isLoading}... // {#if userResource.error}... // {#if userResource.data}... -
Observability: Para depurar o fluxo reativo, a rune
$inspecté uma ferramenta poderosa. Ela permite "espiar" quando os sinais são lidos e quando são atualizados, sem criar uma dependência.$inspect(itemCount, totalPrice); // Loga no console sempre que esses valores são lidos ou mudamPara observabilidade em produção, integre com ferramentas como Sentry ou LogRocket. Você pode criar um
$effectde alto nível que observa estados críticos e envia eventos ou logs quando ocorrem mudanças inesperadas ou erros. -
Performance: A reatividade granular das Runes é, por padrão, muito performática. No entanto, existem armadilhas:
- Objetos Grandes: Se você tem um objeto grande e complexo em um
$statee muda apenas uma propriedade aninhada, o Svelte pode precisar rastrear muitas dependências. Para objetos que devem ser tratados como imutáveis, use$state.frozen(). Isso informa ao Svelte para não tentar fazer um proxy de reatividade profunda, acionando atualizações apenas quando o objeto inteiro é substituído. - Cálculos Caros em
$derived: Um$derivedé re-calculado a cada mudança em suas dependências. Se o cálculo for pesado, pode causar lentidão. Memorize se necessário, embora na maioria dos casos o modelo de push reativo do Svelte já seja eficiente.
- Objetos Grandes: Se você tem um objeto grande e complexo em um
-
Cost: O Svelte é um framework de compilação, então o custo de execução é puramente o JavaScript gerado no cliente e, se usando SvelteKit, a computação no servidor (SSR/serverless). Uma reatividade eficiente com Runes pode reduzir custos ao minimizar o trabalho no lado do servidor para renderizações e no cliente para atualizações do DOM, levando a um Time to Interactive (TTI) menor e potencialmente menor uso de CPU em funções serverless.
-
Limitations:
- Compile-time: Runes são uma construção do compilador. Elas não funcionarão em arquivos
.js/.tsque não são processados pelo Svelte. - Untracked reads: Dentro de um
$effectou$derived, se você precisar ler um valor reativo sem criar uma dependência (para evitar loops infinitos, por exemplo), você deve usar a funçãountrackdo Svelte 5. Isso é um conceito avançado, mas crucial para cenários complexos.
- Compile-time: Runes são uma construção do compilador. Elas não funcionarão em arquivos
Vale a Pena?
A resposta curta é: sim, absolutamente. A mudança para Runes, embora exija uma reaprendizagem para veteranos do Svelte, alinha o framework com os padrões modernos de reatividade e resolve suas limitações mais fundamentais.
Prós:
- Modelo Mental Unificado: A mesma API para estado local, de componente e global.
- Previsibilidade: A reatividade é explícita. Você sabe o que é reativo (
$state) e o que não é. - Performance Granular: Atualizações cirúrgicas no DOM, potencialmente mais rápidas que a invalidação em nível de componente.
- Flexibilidade: A reatividade agora pode viver em qualquer arquivo TypeScript, permitindo uma arquitetura de estado mais limpa e desacoplada.
Contras:
- Curva de Aprendizagem: Desenvolvedores Svelte existentes precisam se adaptar a uma nova sintaxe e modelo mental.
- Verbosidade:
$state(0)é mais verboso quelet count = 0. Este é o preço da clareza. - Maturidade do Ecossistema: O ecossistema de bibliotecas está se adaptando. Embora o Svelte 5 tenha um modo de compatibilidade, as bibliotecas que aproveitam totalmente as Runes ainda estão surgindo.
Quando usar vs. Quando evitar:
- Use Runes: Para todos os novos projetos Svelte 5. A performance e os benefícios de DX superam a pequena verbosidade adicional. Comece a migrar aplicações existentes por componentes de folha (sem filhos) ou ao implementar novas funcionalidades.
- Evite (por enquanto): Se você tem um projeto Svelte 4 massivo e estável e não tem tempo para uma migração gradual, ou se depende de bibliotecas críticas que ainda não são compatíveis com Svelte 5.
O Retorno sobre o Investimento (ROI) vem na forma de menor tempo de depuração (graças à reatividade explícita), maior facilidade em refatorar e escalar a gestão de estado, e melhor performance out-of-the-box.
Conclusão
Svelte 5 Runes não são apenas uma nova feature; são a fundação da próxima década do Svelte.
- Reatividade Explícita: Elas trocam a "mágica" por clareza, tornando o fluxo de dados mais fácil de rastrear e depurar.
- Estado Unificado: O dilema "estado local vs. stores globais" foi resolvido. Agora, há apenas estado reativo, que pode ser usado em qualquer lugar.
- Performance Otimizada: Ao adotar um modelo de sinais, o Svelte 5 possibilita atualizações de DOM extremamente granulares, solidificando sua reputação como um dos frameworks mais performáticos.
Recomendação final: Abrace as Runes. O Svelte manteve sua simplicidade fundamental, mas adicionou o poder e a previsibilidade que os engenheiros que constroem aplicações complexas e de larga escala necessitam. A transição vale o esforço.
Próximos passos acionáveis:
- Inicie um novo projeto com
npm create svelte@next. - Refatore um pequeno projeto Svelte 4 para usar Runes, começando com a substituição de stores por um módulo de estado global.
- Explore o uso de
$effectpara integrações com APIs de terceiros (ex: bibliotecas de gráficos, mapas) e veja como a função de cleanup simplifica o ciclo de vida.
O futuro do Svelte é explícito, granular e incrivelmente poderoso. As Runes são a ferramenta que nos permitirá construir a próxima geração de interfaces web rápidas e resilientes com uma experiência de desenvolvimento incomparável.
Recursos
- Documentação Oficial:
- Introdução às Runes (O post de anúncio de Rich Harris)
- Documentação da API Svelte 5 (Preview da documentação oficial)
- Repositórios GitHub:
- Repositório Principal do Svelte (Acompanhe o desenvolvimento do Svelte 5)
- Comunidades Ativas:
- Svelte Discord Server (O canal
#svelte-5é o melhor lugar para discussões)
- Svelte Discord Server (O canal
- Estudos de Caso e Artigos:
- Svelte 5: The Full Rundown (Vídeo explicativo do Huntabyte)
Este conteúdo foi útil?
Deixe-nos saber o que achou deste post
Comentários
Carregando comentários...