Voltar ao blog

Compartilhe este artigo

Svelte 5 Runes: Guia Definitivo para Estado Global em Produção

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

MonitorFrontend
J
Jonh Alex
16 min de leitura

Ouça este artigo

Svelte 5 Runes: Guia Definitivo para Estado Global em Produção

0:000:00

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:

  1. 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 (exigindo subscribe, update ou o prefixo $).
  2. 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.
  3. 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 useState ou ref podia 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 a let no 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:

  1. Habilitar runes na configuração (svelte.config.js). Em projetos novos com svelte@next já vem por padrão.
  2. Substituir let por $state() para estado reativo.
  3. Substituir $: label por $derived() para valores computados e $effect() para efeitos colaterais.
  4. Substituir export let por $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 dev

1. 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 $state com 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? $derived não tem um mecanismo de try/catch embutido. O padrão é gerenciar o estado de erro separadamente. Para dados assíncronos, use um padrão de "recurso" que encapsula os estados de loading, error e data.

    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 mudam

    Para observabilidade em produção, integre com ferramentas como Sentry ou LogRocket. Você pode criar um $effect de 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 $state e 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.
  • 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/.ts que não são processados pelo Svelte.
    • Untracked reads: Dentro de um $effect ou $derived, se você precisar ler um valor reativo sem criar uma dependência (para evitar loops infinitos, por exemplo), você deve usar a função untrack do Svelte 5. Isso é um conceito avançado, mas crucial para cenários complexos.

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 que let 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.

  1. Reatividade Explícita: Elas trocam a "mágica" por clareza, tornando o fluxo de dados mais fácil de rastrear e depurar.
  2. Estado Unificado: O dilema "estado local vs. stores globais" foi resolvido. Agora, há apenas estado reativo, que pode ser usado em qualquer lugar.
  3. 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:

  1. Inicie um novo projeto com npm create svelte@next.
  2. Refatore um pequeno projeto Svelte 4 para usar Runes, começando com a substituição de stores por um módulo de estado global.
  3. Explore o uso de $effect para 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

Tags:
reatividade
SvelteKit
Svelte 5
Runes
state management

Compartilhe este artigo

Este conteúdo foi útil?

Deixe-nos saber o que achou deste post

Comentários

Deixe um comentário

Carregando comentários...