State Management Reactivo con Svelte Stores
Cómo manejar estado reactivo en Svelte usando writable, readable, derived stores y custom stores con actualizaciones basadas en contrato.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Overview
Svelte stores son contenedores reactivos para valores que necesitan compartirse a través de componentes. Un store es cualquier objeto con un método subscribe que notifica a los suscriptores cuando el valor cambia. Svelte provee writable (read-write), readable (read-only) y derived (computado de otros stores). Los componentes se auto-suscriben a stores usando el prefijo $ en templates, y la suscripción se limpia automáticamente cuando el componente se desmonta.
When to Use
- Compartir estado entre sibling components sin prop drilling
- Global app state (user session, theme, feature flags)
- Estado computado que depende de múltiples otros stores
- Comunicación cross-component (event buses, notification queues)
- Data async que carga una vez y es consumida por múltiples componentes
When NOT to Use
- Estado local del componente — usa las declaraciones
lety reactividad de Svelte directamente - Server-side data fetching — usa las funciones
loadde SvelteKit en su lugar - State machines complejas — considera XState o una librería dedicada
- Cuando pasar props es suficiente para 2-3 niveles — no over-engineeres con stores
Solution
Writable store
<script>
import { writable } from "svelte/store";
// Crear un writable store con valor inicial
const count = writable(0);
function increment() {
count.update((n) => n + 1);
}
function decrement() {
count.update((n) => n - 1);
}
function reset() {
count.set(0);
}
</script>
<h1>Count: {$count}</h1>
<button on:click={increment}>+</button>
<button on:click={decrement}>-</button>
<button on:click={reset}>Reset</button>
La sintaxis $count auto-suscribe en el template. En el script block, usa count.set(), count.update() o get(count).
Store compartido en archivo separado
// stores/user.js
import { writable } from "svelte/store";
export const user = writable(null);
export async function loginUser(credentials) {
const response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify(credentials),
});
const data = await response.json();
user.set(data);
return data;
}
export function logoutUser() {
user.set(null);
}
<!-- Header.svelte -->
<script>
import { user, logoutUser } from "../stores/user.js";
</script>
{#if $user}
<span>Welcome, {$user.name}</span>
<button on:click={logoutUser}>Logout</button>
{:else}
<a href="/login">Login</a>
{/if}
Readable store (read-only con updates internos)
// stores/time.js
import { readable } from "svelte/store";
export const currentTime = readable(new Date(), (set) => {
const interval = setInterval(() => {
set(new Date());
}, 1000);
// Cleanup function llamada cuando el último suscriptor se desuscribe
return () => clearInterval(interval);
});
<script>
import { currentTime } from "../stores/time.js";
</script>
<p>Current time: {$currentTime.toLocaleTimeString()}</p>
El store arranca el interval cuando el primer suscriptor se adjunta y lo detiene cuando el último se va — no hay recursos desperdiciados.
Derived store
// stores/cart.js
import { writable, derived } from "svelte/store";
export const cart = writable([]);
export const itemCount = derived(cart, ($cart) =>
$cart.reduce((sum, item) => sum + item.quantity, 0)
);
export const totalPrice = derived(cart, ($cart) =>
$cart.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
export const formattedTotal = derived(
totalPrice,
($total) => `$${$total.toFixed(2)}`
);
<script>
import { cart, itemCount, formattedTotal } from "../stores/cart.js";
function addItem() {
cart.update((items) => [
...items,
{ id: Date.now(), name: "Product", price: 9.99, quantity: 1 },
]);
}
</script>
<p>Items in cart: {$itemCount}</p>
<p>Total: {$formattedTotal}</p>
<button on:click={addItem}>Add item</button>
Derived de múltiples stores
import { derived } from "svelte/store";
import { user } from "./user.js";
import { settings } from "./settings.js";
export const greeting = derived(
[user, settings],
([$user, $settings]) => {
if (!$user) return "Welcome, guest";
const name = $settings.formal ? $user.fullName : $user.firstName;
return `Hello, ${name}`;
}
);
Custom store con contrato
// stores/counter.js
import { writable } from "svelte/store";
function createCounter(start = 0, step = 1) {
const { subscribe, update, set } = writable(start);
return {
subscribe,
increment: () => update((n) => n + step),
decrement: () => update((n) => n - step),
reset: () => set(start),
setStep: (newStep) => update((n) => n), // step se captura en closure
};
}
export const counter = createCounter(0, 5);
<script>
import { counter } from "../stores/counter.js";
</script>
<h1>{$counter}</h1>
<button on:click={counter.increment}>+5</button>
<button on:click={counter.decrement}>-5</button>
<button on:click={counter.reset}>Reset</button>
Store con persistencia en localStorage
// stores/persistent.js
import { writable } from "svelte/store";
function persistent(key, initialValue) {
const stored = localStorage.getItem(key);
const initial = stored ? JSON.parse(stored) : initialValue;
const store = writable(initial);
store.subscribe((value) => {
localStorage.setItem(key, JSON.stringify(value));
});
return store;
}
export const theme = persistent("theme", "light");
export const sidebarOpen = persistent("sidebar", false);
Auto-unsubscribe en componentes
<script>
import { onMount } from "svelte";
import { user } from "../stores/user.js";
// Suscripción manual (cuando necesitas el valor en lógica de script)
let currentUser;
const unsubscribe = user.subscribe((value) => {
currentUser = value;
});
// Cleanup al desmontar
onDestroy(unsubscribe);
</script>
<!-- Auto-suscripción con prefijo $ (solo template) -->
<h1>{$user?.name}</h1>
Usar get() para lecturas one-time
import { get } from "svelte/store";
import { user } from "../stores/user.js";
function checkAuth() {
const currentUser = get(user);
if (!currentUser) {
throw new Error("Not authenticated");
}
return currentUser;
}
get() lee el valor actual sin suscribirse. Úsalo para lecturas one-time en event handlers — no lo uses en reactive declarations.
Store con updates async
// stores/products.js
import { writable } from "svelte/store";
export const products = writable([]);
export const loading = writable(false);
export const error = writable(null);
export async function fetchProducts() {
loading.set(true);
error.set(null);
try {
const response = await fetch("/api/products");
if (!response.ok) throw new Error("Failed to fetch");
const data = await response.json();
products.set(data);
} catch (err) {
error.set(err.message);
} finally {
loading.set(false);
}
}
<script>
import { onMount } from "svelte";
import { products, loading, error, fetchProducts } from "../stores/products.js";
onMount(fetchProducts);
</script>
{#if $loading}
<p>Loading...</p>
{:else if $error}
<p>Error: {$error}</p>
{:else}
<ul>
{#each $products as product}
<li>{product.name} — ${product.price}</li>
{/each}
</ul>
{/if}
Variants
Usar stores con SvelteKit
// src/stores/session.js
import { writable } from "svelte/store";
export const session = writable({
user: null,
token: null,
});
export function setSession(data) {
session.set(data);
if (data.token) {
document.cookie = `token=${data.token}; path=/; max-age=3600`;
}
}
<!-- +layout.svelte -->
<script>
import { session } from "$lib/stores/session.js";
import { onMount } from "svelte";
onMount(() => {
const token = document.cookie
.split("; ")
.find((c) => c.startsWith("token="))
?.split("=")[1];
if (token) {
fetch("/api/me", { headers: { Authorization: `Bearer ${token}` } })
.then((r) => r.json())
.then((user) => session.set({ user, token }));
}
});
</script>
{#if $session.user}
<slot />
{:else}
<p>Please log in</p>
{/if}
Store con debounce
import { writable } from "svelte/store";
function debouncedStore(initialValue, delay = 300) {
const store = writable(initialValue);
let timeout;
function set(value) {
clearTimeout(timeout);
timeout = setTimeout(() => store.set(value), delay);
}
return {
subscribe: store.subscribe,
set,
update: (fn) => set(fn(get(store))),
};
}
export const searchQuery = debouncedStore("", 300);
Best Practices
-
For a deeper guide, see React State Management: Context, Zustand, TanStack Query.
-
Exporta stores desde archivos separados — mantiene los componentes limpios y los stores reutilizables
-
Usa
readablepara valores que los componentes no deberían modificar (clocks, WebSocket data) -
Usa
derivedpara estado computado — no dupliques data que se puede calcular de otros stores -
Usa el prefijo
$en templates — auto-suscripción/desuscripción es manejada por Svelte -
Usa
get()solo para lecturas one-time en event handlers — no en reactive declarations -
Limpia suscripciones manuales con
onDestroy(unsubscribe)— las auto-suscripciones manejan esto -
Usa custom stores para exponer una API controlada — esconde
setyupdatecuando no deberían ser públicos
Common Mistakes
- Usar
get()en reactive statements:$: doubled = get(count) * 2no reacciona a cambios. Usa$counto un derived store. - No limpiar suscripciones manuales: llamar
store.subscribe()sin guardar la unsubscribe function leakea memoria. - Overusar stores para estado local:
let count = 0es más simple queconst count = writable(0)para estado local del componente. - Crear stores dentro de componentes:
const count = writable(0)dentro de un componente crea un nuevo store en cada render. Muévelo fuera o usaconsta scope de módulo. - No usar derived stores: computar valores en componentes desde múltiples stores manualmente es error-prone —
derivedmaneja los updates automáticamente.
FAQ
¿Qué es el prefijo $ en Svelte?
Es syntactic sugar para auto-suscribirse a un store en templates y reactive declarations. $count es equivalente a suscribirse a count y usar su valor actual. Svelte maneja suscripción y cleanup automáticamente.
¿Puedo usar Svelte stores fuera de componentes Svelte?
Sí. Los stores son objetos JavaScript planos con un método subscribe. Puedes suscribirte manualmente en cualquier archivo JavaScript:
import { user } from "./stores/user.js";
const unsubscribe = user.subscribe((value) => {
console.log("User changed:", value);
});
// Después
unsubscribe();
¿Cuál es el contrato de store?
Cualquier objeto con un método subscribe que retorna una unsubscribe function es un store. El método debe aceptar un callback y llamarlo inmediatamente con el valor actual, luego en cada cambio.
¿Cómo testeo un store?
import { get } from "svelte/store";
import { counter } from "./stores/counter.js";
test("counter increments", () => {
counter.reset();
counter.increment();
expect(get(counter)).toBe(5); // step es 5
});
¿Debería usar stores o context API para árboles de componentes?
Usa context (setContext/getContext) para data scoped a un subtree de componentes — es más simple y no requiere stores. Usa stores para estado global compartido a través de toda la app o cuando necesitas reactividad fuera de componentes.
Recursos Relacionados
Data Fetching with Vue 3 Composition API
How to fetch and manage data in Vue 3 using the Composition API with ref, computed, watch, and composables for reusable data logic.
RecipeWhen to Use useMemo and useCallback
How and when to use React's useMemo and useCallback hooks for performance optimization, and when they add unnecessary overhead.
RecipeContainer Queries for Component Responsiveness
How to use CSS container queries for component-level responsive layouts that adapt to their container size instead of the viewport.
PatternOptimistic Update: Update UI Immediately, Reconcile on
How to update UI immediately and reconcile on server response in React. Covers rollback on error, conflict resolution, and React Query integration.
PatternState Machine UI: Finite State Machines for UI
How to model UI state transitions with finite state machines in React. Covers XState, statecharts, guarded transitions, and preventing impossible states.
GuideReact State Management: Context, Zustand, TanStack Query
Master React state management: Context API, Zustand, Jotai, and TanStack Query for server state. Covers patterns, persistence, optimistic updates, and when to use each.