Volver al blog

Svelte 5 y Runes: La Revolución de la Reactividad en JavaScript en 2025

Hola HaWkers, Svelte 5 llegó con un cambio paradigmático que está haciendo a la comunidad JavaScript repensar cómo abordamos reactividad en aplicaciones web. Los Runes representan una nueva forma de pensar sobre estado y reactividad, y si aún no los conoces, estás perdiendo una de las innovaciones más interesantes del ecosistema frontend.

¿Ya te preguntaste por qué necesitamos tantos hooks y boilerplate para gerenciar estado en React? Svelte 5 ofrece una alternativa elegante.

Qué Son los Runes

Runes son una nueva primitiva de reactividad introducida en Svelte 5. Ellos sustituyen el sistema de reactividad anterior basado en declaraciones reactivas ($:) por un modelo más explícito y poderoso.

Los Principales Runes

Lista de Runes disponibles:

  • $state - Declara estado reactivo
  • $derived - Computa valores derivados
  • $effect - Ejecuta side effects reactivos
  • $props - Recibe props en componentes
  • $bindable - Props que pueden ser two-way bound
  • $inspect - Debug de valores reactivos

Comparación: Svelte 4 vs Svelte 5

Vamos a ver en la práctica cómo la sintaxis cambió y por qué es una mejora significativa.

Estado Simple

Svelte 4 (antiguo):

<script>
  let count = 0;

  function increment() {
    count += 1;
  }
</script>

<button on:click={increment}>
  Clics: {count}
</button>

Svelte 5 (Runes):

<script>
  let count = $state(0);

  function increment() {
    count += 1;
  }
</script>

<button onclick={increment}>
  Clics: {count}
</button>

La diferencia parece sutil, pero el $state torna la reactividad explícita y previsible.

Valores Derivados

Svelte 4 (antiguo):

<script>
  let firstName = 'John';
  let lastName = 'Doe';

  // Declaración reactiva - puede ser confusa
  $: fullName = `${firstName} ${lastName}`;
  $: greeting = `Hola, ${fullName}!`;
</script>

Svelte 5 (Runes):

<script>
  let firstName = $state('John');
  let lastName = $state('Doe');

  // Mucho más claro y previsible
  let fullName = $derived(`${firstName} ${lastName}`);
  let greeting = $derived(`Hola, ${fullName}!`);
</script>

Effects (Side Effects)

Svelte 4 (antiguo):

<script>
  let count = 0;

  // Effect implícito - difícil saber cuándo ejecuta
  $: {
    console.log(`Count cambió para: ${count}`);
    localStorage.setItem('count', count);
  }
</script>

Svelte 5 (Runes):

<script>
  let count = $state(0);

  // Effect explícito - claro cuándo y por qué ejecuta
  $effect(() => {
    console.log(`Count cambió para: ${count}`);
    localStorage.setItem('count', count.toString());
  });
</script>

Ejemplo Práctico: Todo App con Runes

Vamos a construir una aplicación de tareas completa usando Runes:

<script>
  // Estado principal
  let todos = $state([]);
  let newTodo = $state('');
  let filter = $state('all'); // 'all' | 'active' | 'completed'

  // Valores derivados
  let filteredTodos = $derived(() => {
    switch (filter) {
      case 'active':
        return todos.filter(t => !t.completed);
      case 'completed':
        return todos.filter(t => t.completed);
      default:
        return todos;
    }
  });

  let remainingCount = $derived(
    todos.filter(t => !t.completed).length
  );

  let allCompleted = $derived(
    todos.length > 0 && todos.every(t => t.completed)
  );

  // Funciones de acción
  function addTodo() {
    if (newTodo.trim()) {
      todos.push({
        id: Date.now(),
        text: newTodo.trim(),
        completed: false
      });
      newTodo = '';
    }
  }

  function toggleTodo(id) {
    const todo = todos.find(t => t.id === id);
    if (todo) {
      todo.completed = !todo.completed;
    }
  }

  function removeTodo(id) {
    const index = todos.findIndex(t => t.id === id);
    if (index !== -1) {
      todos.splice(index, 1);
    }
  }

  function toggleAll() {
    const newState = !allCompleted;
    todos.forEach(t => t.completed = newState);
  }

  function clearCompleted() {
    todos = todos.filter(t => !t.completed);
  }

  // Effect para persistencia
  $effect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  });

  // Cargar del localStorage en la inicialización
  $effect(() => {
    const saved = localStorage.getItem('todos');
    if (saved) {
      todos = JSON.parse(saved);
    }
  });
</script>

<div class="todo-app">
  <h1>Mis Tareas</h1>

  <form onsubmit={(e) => { e.preventDefault(); addTodo(); }}>
    <input
      type="text"
      bind:value={newTodo}
      placeholder="¿Qué necesita ser hecho?"
    />
    <button type="submit">Agregar</button>
  </form>

  {#if todos.length > 0}
    <div class="controls">
      <button onclick={toggleAll}>
        {allCompleted ? 'Desmarcar' : 'Marcar'} Todas
      </button>

      <div class="filters">
        <button
          class:active={filter === 'all'}
          onclick={() => filter = 'all'}
        >
          Todas
        </button>
        <button
          class:active={filter === 'active'}
          onclick={() => filter = 'active'}
        >
          Activas
        </button>
        <button
          class:active={filter === 'completed'}
          onclick={() => filter = 'completed'}
        >
          Completas
        </button>
      </div>
    </div>

    <ul class="todo-list">
      {#each filteredTodos as todo (todo.id)}
        <li class:completed={todo.completed}>
          <input
            type="checkbox"
            checked={todo.completed}
            onchange={() => toggleTodo(todo.id)}
          />
          <span>{todo.text}</span>
          <button onclick={() => removeTodo(todo.id)}>X</button>
        </li>
      {/each}
    </ul>

    <footer>
      <span>{remainingCount} tarea(s) restante(s)</span>
      {#if todos.some(t => t.completed)}
        <button onclick={clearCompleted}>
          Limpiar Completas
        </button>
      {/if}
    </footer>
  {/if}
</div>

Por Qué Runes Son Mejores

1. Reactividad Explícita

En Svelte 4, cualquier variable let era automáticamente reactiva, lo que podía causar confusión:

<!-- Svelte 4: ¿Cuándo esto es reactivo? -->
<script>
  let data = fetch('/api').then(r => r.json()); // ¿Reactivo?
  let config = { theme: 'dark' }; // ¿Reactivo?
  const MAX = 100; // No reactivo (const)
</script>

Con Runes, declaras explícitamente lo que es reactivo:

<!-- Svelte 5: Claridad total -->
<script>
  let data = $state(null); // Reactivo
  let config = $state({ theme: 'dark' }); // Reactivo
  const MAX = 100; // No reactivo (intencional)
</script>

2. Mejor TypeScript Support

Runes funcionan perfectamente con TypeScript:

<script lang="ts">
  interface Todo {
    id: number;
    text: string;
    completed: boolean;
  }

  let todos = $state<Todo[]>([]);
  let newTodo = $state('');

  // Tipos inferidos correctamente
  let completedCount = $derived(
    todos.filter(t => t.completed).length
  );
</script>

3. Reactividad Universal

Runes funcionan fuera de componentes .svelte:

// stores/counter.svelte.ts
export function createCounter(initial = 0) {
  let count = $state(initial);

  return {
    get value() { return count; },
    increment() { count += 1; },
    decrement() { count -= 1; },
    reset() { count = initial; }
  };
}

// En cualquier componente
import { createCounter } from './stores/counter.svelte';

const counter = createCounter(10);

Comparación con React

Vamos a comparar la misma funcionalidad en React y Svelte 5:

React (Hooks):

import { useState, useMemo, useEffect } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  // useMemo para valores derivados
  const doubled = useMemo(() => count * 2, [count]);

  // useEffect para side effects
  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);

  return (
    <div>
      <p>Count: {count} (doubled: {doubled})</p>
      <input
        type="number"
        value={step}
        onChange={e => setStep(Number(e.target.value))}
      />
      <button onClick={() => setCount(c => c + step)}>
        Increment by {step}
      </button>
    </div>
  );
}

Svelte 5 (Runes):

<script>
  let count = $state(0);
  let step = $state(1);

  // Derivado - sin array de dependencias
  let doubled = $derived(count * 2);

  // Effect - sin array de dependencias
  $effect(() => {
    document.title = `Count: ${count}`;
  });
</script>

<div>
  <p>Count: {count} (doubled: {doubled})</p>
  <input type="number" bind:value={step} />
  <button onclick={() => count += step}>
    Increment by {step}
  </button>
</div>

Diferencias notables:

  • Sin arrays de dependencias (menos bugs)
  • Sintaxis más concisa
  • Binding bidireccional nativo
  • Menos boilerplate

Performance de Svelte 5

Svelte 5 trae mejoras significativas de performance:

Benchmarks (js-framework-benchmark)

Métrica React 19 Vue 3.5 Svelte 5
Create 1000 rows 45ms 38ms 32ms
Update 1000 rows 38ms 32ms 28ms
Select row 3.2ms 2.8ms 2.1ms
Remove row 35ms 30ms 25ms
Create 10000 rows 450ms 380ms 320ms
Bundle size (gzip) 45KB 35KB 3KB

Destaque: Svelte compila para JavaScript vanilla, resultando en bundles significativamente menores.

Migrando de Svelte 4 a Svelte 5

Estrategia de Migración

  1. Actualiza Svelte: npm install svelte@5
  2. Código existente funciona: Svelte 5 es retrocompatible
  3. Migra gradualmente: Convierte componentes uno por vez
  4. Usa el modo legacy: <svelte:options runes={false} />

Herramienta de Migración

Svelte ofrece una herramienta de migración automática:

# Migrar archivos automáticamente
npx sv migrate svelte-5

# Verificar problemas de compatibilidad
npx svelte-check

Patrones de Migración

Stores para Runes:

// Svelte 4: Stores
import { writable, derived } from 'svelte/store';

export const count = writable(0);
export const doubled = derived(count, $count => $count * 2);

// Svelte 5: Runes (stores aún funcionan!)
// Pero Runes son recomendados para código nuevo
let count = $state(0);
let doubled = $derived(count * 2);

Cuándo Usar Svelte 5

Usa Svelte 5 cuando:

  • Inicias nuevo proyecto frontend
  • Necesitas performance máxima
  • Quieres menos boilerplate
  • Prefieres sintaxis más próxima a JavaScript vanilla
  • Necesitas bundles menores

Considera otras opciones cuando:

  • Equipo muy experimentado en React/Vue
  • Ecosistema de bibliotecas es crucial
  • Necesitas SSR complejo (SvelteKit aún es más joven)

Conclusión

Svelte 5 con Runes representa una evolución significativa en la forma como pensamos sobre reactividad en JavaScript. La sintaxis explícita, el mejor soporte a TypeScript y la performance superior hacen de él una elección atractiva para nuevos proyectos.

Si ya trabajas con React o Vue, los conceptos de Runes serán familiares, pero la implementación es más elegante y directa. Vale la pena experimentar en un proyecto personal para sentir la diferencia.

La comunidad JavaScript continúa innovando, y frameworks como Svelte muestran que aún hay mucho espacio para mejoras en la forma como construimos aplicaciones web.

Si quieres explorar más sobre frameworks JavaScript modernos, te recomiendo echar un vistazo al artículo sobre React vs Vue en 2025: Cuál Framework Elegir donde comparamos las principales opciones del mercado.

¡Vamos a por ello! 🦅

Comentarios (0)

Este artículo aún no tiene comentarios 😢. ¡Sé el primero! 🚀🦅

Añadir comentarios