wandres.dev
CREATESIGNAL · el átomo reactivo

Anatomía de un signal

El primitivo createSignal por dentro: la tupla [getter, setter], por qué el getter es una función y no un valor, los tipos Signal, Accessor y Setter, y la idea nuclear de que leer un signal dentro de un ámbito reactivo es suscribirse a él.

⏱ 15 min

Toda la reactividad de SolidJS se levanta sobre un único primitivo: el signal. Un createSignal no es una variable mágica ni un objeto observable al estilo clásico; es un par de funciones —una para leer y otra para escribir— y ese detalle en apariencia menor, que leer sea invocar una función, es justo lo que hace posible el rastreo de dependencias de grano fino. Antes de construir moléculas, disequemos el átomo.

🎯 Al terminar esta lección sabrás
  • Entender la tupla [getter, setter] que devuelve createSignal.
  • Comprender por qué el getter es una función y no un valor directo.
  • Conocer los tipos Signal, Accessor y Setter que gobiernan el primitivo.
  • Interiorizar la idea nuclear: leer dentro de un ámbito reactivo es suscribirse.

El par [getter, setter]

createSignal recibe un valor inicial y devuelve una tupla de exactamente dos elementos: un getter para leer el valor actual y un setter para reemplazarlo. La convención universal es desestructurar esa tupla con un nombre y ese mismo nombre prefijado con set:

import { createSignal } from "solid-js";

const [count, setCount] = createSignal(0);

count();      // 0  -> leer es LLAMAR a la funcion
setCount(5);  // escribir un valor nuevo
count();      // 5

Que sea una tupla y no un objeto { get, set } es deliberado: te obliga a nombrar ambas mitades en el punto de creación y hace visible, de un vistazo, quién puede leer y quién puede escribir. Puedes entregar solo el getter a un componente hijo y guardarte el setter, logrando un valor de solo lectura sin ceremonia alguna.

const [count, setCount] = createSignal(0);
const soloLectura = count;   // reparte lecturas y nada mas
// setCount queda encapsulado: nadie de fuera puede mutar el estado

El getter es una función, no un valor

Aquí está la decisión de diseño que distingue a Solid de casi todo lo demás. En otros sistemas, un estado reactivo es una propiedad: lees state.count y una capa de proxies intercepta el acceso. Solid renuncia al proxy para el signal y te da una función explícita: escribes count(), con paréntesis. ¿Por qué molestarse?

Porque una lectura debe poder hacer dos cosas a la vez: devolver el valor y registrar quién lo está leyendo. Una función es el lugar natural para ese doble trabajo. Cuando llamas a count(), el getter no solo retorna el número: consulta si hay una computación reactiva activa —un efecto, un memo, el propio JSX— y, si la hay, la inscribe como observadora. Un acceso a propiedad plano no tendría dónde colgar ese comportamiento sin recurrir a proxies.

const [count, setCount] = createSignal(0);

console.log(count);    // [Function]  <- la funcion en si, casi nunca lo que quieres
console.log(count());  // 0           <- el valor, invocando el getter
⚠️
Olvidar los paréntesis es el error número uno

Escribir count en lugar de count() no da error por sí solo: pasas la función en vez de su valor. En una plantilla, <p>{count}</p> imprime algo como el código de la función y, peor aún, no reacciona a los cambios. TypeScript te avisará si esperas un número y recibes una función, pero en contextos laxos el fallo pasa silencioso. Regla mental: para leer, siempre paréntesis.

Leer es suscribirse

Esta es la frase que hay que grabar a fuego. En Solid, el acto de llamar a count() dentro de un ámbito reactivo no es una lectura pasiva: es declarar una dependencia. El signal guarda internamente el conjunto de sus observadores; la computación activa guarda la lista de sus fuentes. Leer teje ese enlace bidireccional; escribir lo recorre para notificar.

flowchart LR
A[count llamado dentro de un efecto] --> B[el signal registra al efecto como observer]
B --> C[el efecto guarda al signal como source]
D[setCount con un valor nuevo] --> E[el signal recorre sus observers]
E --> F[el efecto se vuelve a ejecutar]

La consecuencia es que no hay lista de dependencias que declarar. No existe un array como el de useEffect; el sistema descubre las dependencias ejecutando tu código y anotando qué getters se invocaron. Si esta vez leíste count() y name(), dependes de ambos; si un if hace que la próxima vez solo leas count(), la suscripción a name se descarta automáticamente. El grafo es dinámico y siempre exacto.

📖

El getter es un Accessor

Una función () => T. Leer devuelve el valor y, en un ámbito reactivo, suscribe a quien lee. Fuera de ese ámbito es una lectura normal y corriente, sin efectos secundarios.

✍️

El setter es un Setter

Reemplaza el valor y notifica a los observadores. Acepta un valor directo o una función actualizadora que recibe el valor previo y devuelve el siguiente.

🔗

La tupla Signal

createSignal(0) infiere Signal<number>, es decir [Accessor<number>, Setter<number>]. Se desestructura en getter y setter en el punto de creación.

Fuera de un ámbito reactivo —en el cuerpo de una función normal, en un manejador de evento— llamar a count() simplemente devuelve el valor sin suscribir a nadie. La misma función se comporta como lectura reactiva o como lectura inerte según dónde la invoques. Esa dualidad, que exploraremos en la próxima lección, es la fuente tanto del poder de Solid como de sus confusiones más típicas.

Un signal es una función, y por eso la reactividad compone

Reducir el estado reactivo a una función tiene una elegancia que solo se aprecia al ver lo que desbloquea. Como count es un simple valor de primera clase —una función—, puedes pasarlo, devolverlo, guardarlo en un array, cerrarlo en otra función o componerlo sin que el sistema reactivo se entere ni le importe. No hay un objeto especial que “envuelva” el estado y que debas mantener intacto; hay una llamada, y la llamada lleva consigo, allá donde ocurra, su capacidad de suscribir al lector. De ahí nace el patrón de las señales derivadas: const doble = () => count() * 2 es, sin más aparato, otra función que al invocarse lee count() y por tanto hereda su reactividad. No hizo falta ningún primitivo extra; bastó una función que llama a otra. Esta es la diferencia profunda con los modelos basados en proxies o en clases: allí la reactividad vive en el objeto y se pierde en cuanto lo desestructuras o lo copias; aquí vive en el acto de llamar, y llamar es lo más portable que existe en JavaScript. Interioriza esto y la mitad de Solid deja de ser API para volverse consecuencia: memos, efectos, stores y hasta el JSX no son más que lugares que, al ejecutarse, llaman a tus getters y se apuntan a la lista de dependencias. El átomo es una función; todo lo demás es química.

⚔️ Disecciona el átomo
  1. Crea const [count, setCount] = createSignal(0) e imprime count y count() por separado; explica en una frase la diferencia entre la función y su valor.
  2. Escribe una señal derivada const doble = () => count() * 2 y comprueba que refleja los cambios de count sin ninguna API extra.
  3. Reparte solo el getter a una función auxiliar y guárdate el setter; razona por qué eso es “solo lectura” gratis.
  4. Provoca a propósito el bug de los paréntesis: usa count sin llamar en una plantilla y observa qué se imprime y por qué no reacciona.
  5. Anota con tus palabras qué significa “leer es suscribirse” y en qué se diferencia de leer una variable normal.