Volver al blog

Signals en JavaScript: El Futuro de la Reactividad Que Puede Cambiar la Web

Hola HaWkers, si trabajas con frameworks JavaScript modernos, probablemente ya escuchaste hablar de Signals. Lo que comenzo como un enfoque alternativo para la gestion de estado se esta convirtiendo en un consenso entre Angular, Vue, Solid, Svelte y potencialmente entrando en la especificacion oficial de JavaScript.

Vamos a entender que son Signals, por que estan conquistando la comunidad y como pueden cambiar la forma en que escribimos aplicaciones web.

Que Son Signals

Concepto Fundamental

Signals son primitivas reactivas que almacenan un valor y notifican automaticamente a sus dependientes cuando ese valor cambia. A diferencia del modelo tradicional de re-renderizado completo, Signals permiten actualizaciones quirurgicas solo donde es necesario.

Anatomia basica de un Signal:

// Creando un signal
const count = signal(0)

// Leyendo el valor
console.log(count()) // 0

// Actualizando el valor
count.set(1)
// o
count.update(n => n + 1)

// Derivando valores (computed)
const doubled = computed(() => count() * 2)

// Reaccionando a cambios (effect)
effect(() => {
  console.log(`Count is: ${count()}`)
})

Por Que Signals Son Diferentes

La magia de Signals esta en la reactividad fina (fine-grained reactivity). Compara con enfoques tradicionales:

React (re-render completo):

function Counter() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('User')

  // Cuando count cambia, TODO el componente re-renderiza
  // Incluyendo partes que solo dependen de name
  return (
    <div>
      <h1>Hello, {name}</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  )
}

Con Signals (update quirurgico):

function Counter() {
  const count = signal(0)
  const name = signal('User')

  // Cuando count cambia, SOLO el texto del count actualiza
  // El h1 con name ni se toca
  return (
    <div>
      <h1>Hello, {name()}</h1>
      <p>Count: {count()}</p>
      <button onClick={() => count.update(c => c + 1)}>+</button>
    </div>
  )
}

La Convergencia de los Frameworks

Quien Esta Usando Signals

En 2026, practicamente todos los frameworks JavaScript han adoptado o estan adoptando Signals.

Estado por framework:

Framework Estado Signals Desde
Solid.js Nativo (pionero) 2021
Angular Signals API 2023
Vue 3.6 Alien Signals 2026
Svelte 5 Runes ($state) 2024
Preact @preact/signals 2022
Qwik Signals nativos 2022
React En discusion -

La Propuesta TC39

Lo mas interesante es que Signals pueden convertirse en parte del JavaScript nativo. Hay una propuesta activa en TC39 para agregar Signals a la especificacion.

Estado de la propuesta:

TC39 Proposal: Signals
Stage: 1 (Proposal)
Champions: Daniel Ehrenberg, Rob Riggs

Objetivo: Crear una primitiva de reactividad estandarizada
que funcione como base para todos los frameworks

Propuesta de API nativa:

// Posible API nativa (especulativa)
const count = Signal.state(0)
const doubled = Signal.computed(() => count.get() * 2)

Signal.effect(() => {
  console.log(doubled.get())
})

count.set(5) // Logs: 10

Como Funcionan los Signals Por Dentro

El Sistema de Dependencias

Signals usan un grafo de dependencias que se construye automaticamente durante la ejecucion.

// Internamente, algo asi sucede:

class Signal {
  #value
  #subscribers = new Set()

  constructor(initialValue) {
    this.#value = initialValue
  }

  get() {
    // Si estamos dentro de un effect/computed,
    // registra esta dependencia
    if (currentTracking) {
      this.#subscribers.add(currentTracking)
    }
    return this.#value
  }

  set(newValue) {
    if (this.#value !== newValue) {
      this.#value = newValue
      // Notifica a todos los subscribers
      this.#subscribers.forEach(sub => sub.notify())
    }
  }
}

Tracking Automatico

El tracking de dependencias sucede automaticamente cuando lees un signal dentro de un contexto reactivo.

const firstName = signal('John')
const lastName = signal('Doe')
const age = signal(30)

// Este computed depende SOLO de firstName y lastName
// age no es una dependencia porque no fue leido
const fullName = computed(() => {
  return `${firstName()} ${lastName()}`
})

// Cambiar age NO recalcula fullName
age.set(31) // fullName no reacciona

// Cambiar firstName recalcula fullName
firstName.set('Jane') // fullName recalcula

Push vs Pull

Signals usan un modelo hibrido push-pull:

// PUSH: Cuando un signal cambia, "empuja" notificacion
// a sus dependientes directos

const a = signal(1)
const b = computed(() => a() * 2) // b depende de a
const c = computed(() => b() + 10) // c depende de b

a.set(2) // Push: a notifica b, b notifica c

// PULL: Valores solo se recalculan cuando se leen
// (evaluacion perezosa)

console.log(c()) // Pull: c calcula b, b calcula a

Implementaciones Practicas

Signals en Angular

Angular introdujo Signals como alternativa a RxJS para casos simples.

import { signal, computed, effect } from '@angular/core'

@Component({
  selector: 'app-counter',
  template: `
    <h1>Count: {{ count() }}</h1>
    <h2>Doubled: {{ doubled() }}</h2>
    <button (click)="increment()">+</button>
  `
})
export class CounterComponent {
  count = signal(0)
  doubled = computed(() => this.count() * 2)

  constructor() {
    effect(() => {
      console.log(`Count changed to: ${this.count()}`)
    })
  }

  increment() {
    this.count.update(n => n + 1)
  }
}

Signals en Solid.js

Solid fue el pionero en Signals modernos.

import { createSignal, createEffect, createMemo } from 'solid-js'

function Counter() {
  const [count, setCount] = createSignal(0)
  const doubled = createMemo(() => count() * 2)

  createEffect(() => {
    console.log(`Count is ${count()}`)
  })

  return (
    <div>
      <p>Count: {count()}</p>
      <p>Doubled: {doubled()}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  )
}

Signals en Vue 3.6

Vue esta introduciendo Signals a traves del Vapor Mode.

<script setup>
import { signal, computed, effect } from 'vue/vapor'

const count = signal(0)
const doubled = computed(() => count() * 2)

effect(() => {
  console.log(`Count: ${count()}`)
})

function increment() {
  count.set(count() + 1)
}
</script>

<template>
  <div>
    <p>Count: {{ count() }}</p>
    <p>Doubled: {{ doubled() }}</p>
    <button @click="increment">+</button>
  </div>
</template>

Patrones Avanzados con Signals

Signals Derivados Complejos

// Multiples dependencias
const items = signal([
  { name: 'Apple', price: 1.5, qty: 3 },
  { name: 'Banana', price: 0.75, qty: 5 },
])

const taxRate = signal(0.1)

// Computed que depende de items y taxRate
const total = computed(() => {
  const subtotal = items().reduce(
    (sum, item) => sum + item.price * item.qty,
    0
  )
  return subtotal * (1 + taxRate())
})

// Actualizar cualquier dependencia recalcula total
items.update(i => [...i, { name: 'Orange', price: 2, qty: 2 }])

Effects con Cleanup

// Effects pueden retornar funcion de cleanup
const userId = signal(1)

effect(() => {
  const id = userId()

  // Setup: conectar WebSocket
  const ws = new WebSocket(`/user/${id}/updates`)

  ws.onmessage = (event) => {
    console.log('Update:', event.data)
  }

  // Cleanup: desconectar cuando userId cambie
  return () => {
    ws.close()
  }
})

userId.set(2) // Cierra WS antiguo, abre nuevo

Batching de Updates

// Multiples updates en secuencia
const a = signal(1)
const b = signal(2)
const c = signal(3)

const sum = computed(() => a() + b() + c())

effect(() => {
  console.log('Sum:', sum())
})

// Sin batching: 3 recalculos
a.set(10)
b.set(20)
c.set(30)

// Con batching: 1 recalculo
batch(() => {
  a.set(10)
  b.set(20)
  c.set(30)
})

Signals vs Otros Enfoques

Comparacion con Redux

// Redux: Mucho boilerplate
const store = createStore(reducer)
const mapStateToProps = state => ({ count: state.count })
const mapDispatchToProps = { increment }
connect(mapStateToProps, mapDispatchToProps)(Component)

// Signals: Directo al punto
const count = signal(0)
const increment = () => count.update(n => n + 1)

Comparacion con RxJS

// RxJS: Poderoso pero complejo
const count$ = new BehaviorSubject(0)
const doubled$ = count$.pipe(map(n => n * 2))
doubled$.subscribe(console.log)
count$.next(5)

// Signals: Mas simple para casos comunes
const count = signal(0)
const doubled = computed(() => count() * 2)
effect(() => console.log(doubled()))
count.set(5)

Cuando Usar Cada Enfoque

Escenario Mejor Opcion
Estado local de componente Signals
Streams de datos complejos RxJS
Estado global simple Signals
Estado global complejo Redux/Zustand
Datos asincronos Signals + async
Event sourcing RxJS

Performance de Signals

Benchmarks

Signals ofrecen rendimiento superior en escenarios de updates frecuentes.

Prueba: 10,000 updates en cascada:

Enfoque Tiempo Memoria
Signals (Solid) 12ms 2.1MB
Signals (Preact) 15ms 2.4MB
Vue 3 (ref) 45ms 4.2MB
React (useState) 180ms 8.5MB
MobX 28ms 3.8MB

Por que Signals son rapidos:

  1. Sin VDOM diff - Updates directos al DOM
  2. Tracking preciso - Solo recalcula lo necesario
  3. Evaluacion perezosa - Computa solo cuando se lee
  4. Batching automatico - Agrupa updates

Optimizaciones Comunes

// 1. Evita computeds innecesarios
// Malo
const doubled = computed(() => count() * 2)
const quadrupled = computed(() => doubled() * 2) // Encadenamiento

// Mejor
const quadrupled = computed(() => count() * 4)

// 2. Usa untrack para lecturas no reactivas
import { untrack } from 'solid-js'

effect(() => {
  const currentCount = count()
  // Esta lectura NO crea dependencia
  const config = untrack(() => configSignal())
  console.log(currentCount, config)
})

// 3. Memoiza computaciones pesadas
const expensiveResult = computed(() => {
  return items().map(item => heavyComputation(item))
}, { equals: deepEquals })

El Futuro de los Signals

Estandarizacion TC39

Si la propuesta TC39 es aprobada, tendremos Signals nativos en JavaScript.

Timeline estimado:

  • 2024: Stage 1 (actual)
  • 2025: Stage 2 (Draft)
  • 2026-2027: Stage 3 (Candidate)
  • 2028+: Stage 4 (Finished)

Beneficios de la estandarizacion:

  1. Interoperabilidad entre frameworks
  2. Performance optimizada por los engines
  3. Menos dependencias externas
  4. API consistente

Impacto en los Frameworks

Con Signals nativos, frameworks podrian:

// Hoy: Cada framework tiene su implementacion
import { signal } from 'solid-js' // Solid
import { ref } from 'vue' // Vue
import { signal } from '@angular/core' // Angular

// Futuro: Todos usan la misma base
import { Signal } from 'std:signals' // Nativo

React y Signals

React todavia no ha adoptado Signals oficialmente, pero hay discusiones.

Perspectivas:

  • React Forget (compiler) resuelve parte del problema
  • Signals contradiciria el modelo mental de React
  • Posible adopcion parcial o biblioteca oficial
  • Comunidad ya usa @preact/signals-react

Conclusion

Signals representan una evolucion natural en la gestion de estado JavaScript. La convergencia de los principales frameworks alrededor de esta primitiva, combinada con la propuesta TC39, sugiere que Signals seran parte fundamental del desarrollo web en los proximos anos.

Puntos principales:

  1. Signals ofrecen reactividad fina y performante
  2. Angular, Vue, Solid, Svelte ya adoptaron
  3. Propuesta TC39 puede hacer Signals nativos
  4. Performance superior a enfoques tradicionales
  5. API simple e intuitiva

Recomendaciones:

  • Aprende la API de Signals de tu framework
  • Experimenta con Solid.js para entender Signals puros
  • Acompana la propuesta TC39
  • Considera Signals para nuevos proyectos

El futuro de la reactividad en la web esta siendo definido ahora, y Signals estan en el centro de esta transformacion.

Para mas sobre evolucion de frameworks, lee: Vue 3.6 Vapor Mode: La Revolucion de Rendimiento.

Vamos con todo! 🦅

Comentarios (0)

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

Añadir comentarios