Saltar al contenido principal

Reactividad

Estado

  • Svelte tiene un sistema de reactividad para mantener el DOM sincronizado con el estado de la aplicación, por ejemplo, en respuesta a un evento.
  • Generalmente son estados internos (los valores solo son accesibles dentro de un componente determinado) (sin mencionar el estado compartido o universal)
  • Se define la variable, y la asignación de la variable se envuelve en $state(x)
  • Se llamaruna y es la forma de decirle a Svelte que no es una variable común
  • Las runas parecen funciones pero no lo son
<script>
let count = $state(0); // Runa; ya es reactivo

function increment() {
count += 1; // Reasignaciones; se actualiza en automático en la UI
}
</script>

<button onclick={increment}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

Estado compartido o universal

  • Las runas se pueden usar dentro de los componentes .svelte para añadir reactividad
  • También se pueden usar runas fuera de los componentes, por ejemplo, para compartir un estado global
  • Para poder usar runas fuera de los componentes, la extensión del archivo debe ser <name>.svelte.ts (no se pueden usar runas en archivos normales JS/TS)

Estado global en un archivo compartido, por ejemplo shared.svelte.ts

src/shared.svelte.ts
export const counter = $state({
count: 0
});

Uso y modificación del estado

src/Counter.svelte
<script>
import { counter } from './shared.svelte.js';
</script>

<button onclick={() => counter.count += 1}>
clicks: {counter.count}
</button>

Se utiliza el mismo componente varias veces, pero siguen compartiendo estado

src/App.svelte
<script>
import Counter from './Counter.svelte';
</script>

<Counter />
<Counter />
<Counter />

Reasignación vs. mutación en estado exportado

  • No se puede exportar una declaración $state si se reasigna (count = count + 1), porque los que hacen import no tienen forma de enterarse del cambio (solo reciben el valor, no una conexión en vivo a la variable).
  • Si en cambio se muta un objeto/array (counter.count += 1), todos los importadores siguen apuntando al mismo objeto/Proxy, así que sí ven el cambio.

Regla práctica: para compartir estado entre archivos, envolver el valor en un objeto y mutar sus propiedades, en vez de exportar un primitivo suelto y reasignarlo.

// Mal: primitivo exportado, reasignarlo no se propaga a los importadores
export let count = $state(0);
export function increment() {
count += 1; // reasignación
}
// Bien: objeto exportado, se muta su propiedad
export const counter = $state({ count: 0 });
export function increment() {
counter.count += 1; // mutación
}

Deep state

  • En el ejemplo anterior, el estado reacciona a las reasignaciones
  • El estado también puede reaccionar a las mutaciones, llamado deep state
  • Mutación no es un método ni una característica especial de Svelte, es un concepto general de JavaScript: modificar el contenido de un objeto/array sin reemplazar la variable completa.
<script>
let numbers = $state([1, 2, 3, 4]);

function addNumber() {
// Change the array
//numbers[numbers.length] = numbers.length + 1;
numbers.push(numbers.length + 1)
}
</script>

<p>{numbers.join(' + ')} = ...</p>

<button onclick={addNumber}>
Add a number
</button>

let numbers = $state([1, 2, 3, 4]);
Esto crea un array reactivo. numbers no es un array "normal" de JS, es un Proxy que envuelve [1, 2, 3, 4]. Cualquier operación sobre numbers (leer, escribir, push, etc.) pasa primero por ese Proxy.

numbers[numbers.length] = numbers.length + 1;
Desglose con un ejemplo concreto:

  • numbers es [1, 2, 3, 4], entonces numbers.length es 4.
  • La línea se convierte en: numbers[4] = 5;
  • O sea: "pon el valor 5 en la posición 4" (la posición justo después del último elemento).
  • Resultado: numbers ahora es [1, 2, 3, 4, 5].

No se reasignó numbers (no se escribió numbers = algo). Solo se modificó una casilla del array existente. Eso es una mutación. Como numbers es un Proxy, este intercepta esa escritura (numbers[4] = 5), se da cuenta de que cambió, y le dice a Svelte "actualiza la UI". Por eso el <p> se refresca solo, sin necesidad de reasignar.

La alternativa con push:

function addNumber() {
numbers.push(numbers.length + 1);
}

Hace exactamente lo mismo pero de forma más natural: numbers.push(5) agrega 5 al final. push también es un método que muta el array (no crea uno nuevo), así que el Proxy lo intercepta igual y dispara la actualización.

¿Por qué la doc muestra primero la versión rara (numbers[numbers.length] = ...) en vez de ir directo a push?
Para demostrar el punto de forma explícita: quieren probar que cualquier forma de mutación funciona, no solo los métodos "conocidos" como push. Escribir directamente en un índice del array es la mutación más "cruda" posible en JS, y si Svelte detecta eso, seguro detecta también push, splice, pop, etc. Es una forma de decir: "no es magia solo para métodos específicos, es reactividad genuina a nivel de mutación".

Derived state

  • Se utiliza para derivar un estado de otro (reaccionar a otro estado)
  • La expresión dentro de la declaración $derived se volverá a evaluar cada vez que el estado al que hace referencia, cambie
<script>
// Original state
let numbers = $state([1, 2, 3, 4]);

// It reacts when the other state changes
let total = $derived(numbers.reduce((t, n) => t + n, 0));

function addNumber() {
numbers.push(numbers.length + 1);
}
</script>

<p>{numbers.join(' + ')} = {total}</p>

<button onclick={addNumber}>
Add a number
</button>