Signals en JavaScript 2026: El Futuro de la Reactividad Web
Hola HaWkers, una de las propuestas más esperadas del TC39 está avanzando en 2026: Signals nativos en JavaScript. Esta feature promete unificar cómo los frameworks manejan la reactividad y puede cambiar fundamentalmente la forma en que construimos aplicaciones web.
Vamos a explorar qué son los Signals, cómo funcionan y su impacto en el ecosistema.
Qué Son los Signals
Concepto Fundamental
// Signal es un contenedor reactivo para valores
// Analogía simple
const traditionalVariable = 5; // Valor estático
// Si cambia, nadie lo sabe
// Signal
const count = signal(5); // Valor reactivo
// Cuando cambia, los dependientes son notificados automáticamenteCómo Funcionan
// Implementación conceptual (no es la API final del TC39)
// 1. Crear un signal
const count = signal(0);
// 2. Leer el valor
console.log(count.value); // 0
// 3. Escribir un valor
count.value = 1; // Notifica a los dependientes
// 4. Computed (derivado de otros signals)
const doubled = computed(() => count.value * 2);
// doubled.value es siempre count.value * 2
// Recalcula automáticamente cuando count cambia
// 5. Effect (side effect reactivo)
effect(() => {
console.log(`Count es ahora: ${count.value}`);
});
// Se ejecuta siempre que count cambia
Por Qué Importan los Signals
El Problema Actual
// React: re-render del componente entero
function Counter() {
const [count, setCount] = useState(0);
// Cuando count cambia:
// 1. La función Counter se ejecuta de nuevo
// 2. Se crea el Virtual DOM
// 3. Diff con el DOM anterior
// 4. Se aplican los patches
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {count * 2}</p>
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</div>
);
}
// Aunque solo count cambió, todo se re-renderiza// Signals: actualización granular
function Counter() {
const count = signal(0);
const doubled = computed(() => count.value * 2);
// Cuando count cambia:
// 1. Solo el texto que usa count.value se actualiza
// 2. El computed doubled recalcula
// 3. Solo el texto que usa doubled se actualiza
// 4. Ningún re-render de componente
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => count.value++}>
Increment
</button>
</div>
);
}
// Actualización quirúrgica, sin overheadComparación de Performance
// Benchmark conceptual
const performanceComparison = {
react: {
update: 'Re-render de componente',
complexity: 'O(tamaño del árbol)',
overhead: 'Diff del Virtual DOM',
ideal: 'UIs con muchos cambios coordinados'
},
signals: {
update: 'Mutación directa del DOM',
complexity: 'O(1) por signal',
overhead: 'Tracking de dependencias',
ideal: 'Updates frecuentes y granulares'
},
realWorld: {
smallApp: 'Diferencia despreciable',
largeApp: 'Signals 2-10x más rápido',
animations: 'Signals muy superior',
forms: 'Signals muy superior'
}
};
Frameworks Que Ya Usan Signals
SolidJS
// SolidJS: Signals desde el inicio
import { createSignal, createEffect, createMemo } from 'solid-js';
function Counter() {
// Signal primitivo
const [count, setCount] = createSignal(0);
// Computed (memo)
const doubled = createMemo(() => count() * 2);
// Effect
createEffect(() => {
console.log('Count cambió:', count());
});
return (
<div>
<p>Count: {count()}</p>
<p>Doubled: {doubled()}</p>
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</div>
);
}
// Nota: count() es una función, no .value
// SolidJS usa funciones para el trackingVue 3
// Vue 3: Composition API con reactividad basada en Signals
import { ref, computed, watchEffect } from 'vue';
// ref es esencialmente un Signal
const count = ref(0);
// computed es un Signal derivado
const doubled = computed(() => count.value * 2);
// watchEffect es un Effect
watchEffect(() => {
console.log('Count cambió:', count.value);
});
// En template Vue
// <template>
// <p>Count: {{ count }}</p>
// <p>Doubled: {{ doubled }}</p>
// <button @click="count++">Increment</button>
// </template>Angular
// Angular 17+: Signals oficiales
import { signal, computed, effect } from '@angular/core';
@Component({
template: `
<p>Count: {{ count() }}</p>
<p>Doubled: {{ doubled() }}</p>
<button (click)="increment()">Increment</button>
`
})
export class CounterComponent {
// Signal
count = signal(0);
// Computed
doubled = computed(() => this.count() * 2);
constructor() {
// Effect
effect(() => {
console.log('Count cambió:', this.count());
});
}
increment() {
this.count.update(c => c + 1);
// o: this.count.set(this.count() + 1);
}
}Preact Signals
// Preact Signals: ¡funciona en React también!
import { signal, computed, effect } from '@preact/signals-react';
// Signals globales (fuera del componente)
const count = signal(0);
const doubled = computed(() => count.value * 2);
function Counter() {
// ¡Sin hooks! Signal es global y reactivo
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => count.value++}>
Increment
</button>
</div>
);
}
// El componente NO se re-renderiza cuando count cambia
// Solo el texto se actualiza directamente
La Propuesta TC39
Estado en 2026
// TC39 Signals Proposal
const tc39Proposal = {
stage: 'Stage 2 (borrador)',
champions: ['Rob Eisenberg (Angular)', 'Daniel Ehrenberg'],
goal: 'Primitivas estándar para reactividad',
apis: {
// Signal básico
'Signal.State': 'Valor reactivo mutable',
'Signal.Computed': 'Valor derivado (read-only)',
'Signal.subtle.Watch': 'API de bajo nivel para effects',
},
notIncluded: {
effect: 'Los frameworks implementan',
batch: 'Los frameworks implementan',
rendering: 'Los frameworks implementan'
},
philosophy: 'Interoperabilidad, no sustitución'
};API Propuesta
// API TC39 (sujeta a cambios)
// Signal.State - valor mutable
const count = new Signal.State(0);
count.get(); // 0
count.set(1);
count.get(); // 1
// Signal.Computed - valor derivado
const doubled = new Signal.Computed(() => count.get() * 2);
doubled.get(); // 2
// doubled.set() no existe - es read-only
// Signal.subtle.Watch - para frameworks
const watcher = new Signal.subtle.Watch(() => {
// Se llama cuando las dependencias cambian
console.log('Algo cambió');
});
// Los frameworks usan esto para implementar effectsPor Qué Estandarizar
// El problema de la fragmentación actual
const currentFragmentation = {
solidjs: 'createSignal() retorna tuple',
vue: 'ref() usa .value',
angular: 'signal() usa funciones get/set',
preact: 'signal() usa .value',
svelte5: 'Runes con $state',
qwik: 'useSignal() con .value'
};
// Con TC39 Signals
const futureInterop = {
sharedPrimitive: 'Signal.State y Signal.Computed',
frameworks: 'Wrappers finos sobre primitivas estándar',
libraries: 'Pueden ser framework-agnostic',
benefit: 'Compartir código reactivo entre frameworks'
};
Signals vs useState
Cuándo Usar Cada Uno
// React con useState (estándar actual)
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [errors, setErrors] = useState({});
// Cada setState causa re-render
return (
<form>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<input
value={email}
onChange={e => setEmail(e.target.value)}
/>
{/* Re-renderiza todo en cada keystroke */}
</form>
);
}
// Con Signals (ej: Preact Signals en React)
const name = signal('');
const email = signal('');
const errors = signal({});
function Form() {
// ¡El componente no se re-renderiza!
return (
<form>
<input
value={name}
onInput={e => name.value = e.target.value}
/>
<input
value={email}
onInput={e => email.value = e.target.value}
/>
{/* Updates granulares, sin re-render */}
</form>
);
}Trade-offs
// Cuándo los Signals brillan
const signalsIdealFor = [
'Formularios con muchos campos',
'Animaciones y transiciones',
'Updates de alta frecuencia',
'Estado compartido entre componentes',
'Apps con mucha interactividad'
];
// Cuándo useState es suficiente
const useStateIdealFor = [
'Estado local simple',
'UIs con pocos cambios',
'Cuando el re-render es barato',
'Cuando necesitas efectos en el re-render'
];
// Consideraciones
const considerations = {
devex: 'Signals requiere modelo mental diferente',
debugging: 'useState más fácil de debuguear',
ecosystem: 'El ecosistema React asume useState',
future: 'React puede añadir signals nativos'
};
Implementando Signals Hoy
En React
// Opción 1: Preact Signals
npm install @preact/signals-react
import { signal, computed } from '@preact/signals-react';
// Opción 2: Jotai (signal-like)
npm install jotai
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [doubled] = useAtom(doubledAtom);
// ...
}
// Opción 3: Legend State
npm install @legendapp/state @legendapp/state/react
import { observable } from '@legendapp/state';
import { observer } from '@legendapp/state/react';
const state = observable({ count: 0 });
const Counter = observer(function Counter() {
return <div>{state.count.get()}</div>;
});En Vue
// Vue 3 ya tiene signals (refs)
import { ref, computed, watch } from 'vue';
// Composable reactivo
function useCounter() {
const count = ref(0);
const doubled = computed(() => count.value * 2);
function increment() {
count.value++;
}
return { count, doubled, increment };
}
// Uso en componente
export default {
setup() {
const { count, doubled, increment } = useCounter();
return { count, doubled, increment };
}
};En Svelte 5
// Svelte 5 Runes (signals)
<script>
// $state es un signal
let count = $state(0);
// $derived es un computed
let doubled = $derived(count * 2);
// $effect es un effect
$effect(() => {
console.log('Count es', count);
});
function increment() {
count++; // ¡Así de simple!
}
</script>
<button onclick={increment}>
Count: {count}
</button>
<p>Doubled: {doubled}</p>
El Futuro Con TC39 Signals
Escenario 2027+
// Posible futuro con Signals nativos
// JavaScript nativo (¡sin imports!)
const count = new Signal.State(0);
const doubled = new Signal.Computed(() => count.get() * 2);
// React podría adoptar
function Counter() {
// hook use() para signals
const countValue = use(count);
return <p>Count: {countValue}</p>;
}
// O incluso JSX nativo
<p>Count: {count}</p> // Auto-unwrap en JSX
// Bibliotecas framework-agnostic
class ReactiveDatePicker {
selectedDate = new Signal.State(null);
formattedDate = new Signal.Computed(() =>
this.selectedDate.get()?.toLocaleDateString()
);
// Funciona en React, Vue, Angular, Svelte...
}Impacto en el Ecosistema
// Lo que cambia con Signals nativos
const ecosystemImpact = {
frameworks: {
change: 'Wrappers finos sobre primitivas nativas',
benefit: 'Menos código, más performance',
risk: 'Menor diferenciación entre frameworks'
},
libraries: {
change: 'Pueden ser framework-agnostic',
benefit: 'Escribe una vez, usa en cualquier lugar',
example: 'Biblioteca de forms que funciona en React y Vue'
},
learning: {
change: 'El concepto de signal se vuelve fundamental',
benefit: 'Transferencia de conocimiento entre frameworks',
requirement: 'Entender reactividad se vuelve esencial'
},
performance: {
change: 'Optimizaciones en el motor JavaScript',
benefit: 'Signals más rápidos que cualquier implementación userland',
expectation: 'V8, SpiderMonkey optimizan para signals'
}
};Conclusión
Los Signals representan un cambio fundamental en cómo pensamos sobre la reactividad en la web. Mientras que el modelo de re-render de React dominó la última década, los signals ofrecen una alternativa más granular y performante.
Qué hacer ahora:
- Entiende el concepto: Incluso sin usarlos, entiende cómo funcionan los signals
- Experimenta: Prueba Preact Signals, Vue 3 o Angular 17+
- Sigue el TC39: La propuesta está avanzando
- No abandones React: Los signals pueden complementar, no sustituir
El futuro parece converger hacia los signals como primitiva estándar de reactividad. Los frameworks seguirán existiendo, pero con una base común que facilita la interoperabilidad y el aprendizaje.
Para entender más sobre el ecosistema JavaScript actual, lee: VoidZero en 2026.

