wandres.dev
UNTRACK, BATCH, ON · controlar el tracking

createReaction: observación manual

createReaction separa el rastreo de la reacción: llamas a track con una lectura para elegir qué observar, y tu callback se dispara una sola vez cuando cualquiera de esas fuentes cambia, sin re-suscribirse. Cómo re-armar la observación, en qué se diferencia de createEffect y de on con defer, y sus casos de uso avanzados para reacciones de un solo disparo.

⏱ 16 min

Todas las computaciones que has visto —efectos, memos, on— comparten un rasgo: se re-suscriben automáticamente en cada ejecución, viviendo en un ciclo perpetuo de leer y reaccionar. createReaction rompe ese ciclo. Es la primitiva de más bajo nivel del arsenal reactivo de Solid, y su singularidad es doble: separa el acto de observar del acto de reaccionar en dos funciones distintas, y dispara su reacción una sola vez, sin re-armarse sola. Es la herramienta para cuando no quieres un vigilante permanente, sino una alarma de un solo uso.

🎯 Al terminar esta lección sabrás
  • Entender que createReaction separa el rastreo (track) de la reacción (el callback).
  • Comprender que reacciona una única vez y no se re-suscribe por sí misma.
  • Aprender a re-armar la observación llamando a track de nuevo desde el callback.
  • Contrastarla con createEffect y con on para elegir la primitiva correcta.

Rastrear y reaccionar, en dos piezas separadas

createReaction recibe el callback de reacción y te devuelve una función track. La reacción no corre al crearla: primero eliges qué observar llamando a track con una función que lee signals; a partir de ahí, el callback se disparará la primera vez que cualquiera de esas fuentes cambie.

import { createSignal, createReaction } from "solid-js";

const [dato, setDato] = createSignal("inicial");

const track = createReaction(() => {
  console.log("¡algo que observé cambió!");
});

track(() => dato()); // ahora observa a dato

setDato("nuevo");    // dispara el callback UNA vez
setDato("otro");     // ya no dispara: la reacción se consumió

Fíjate en la asimetría con createEffect. En un efecto, el mismo cuerpo lee y reacciona, y se re-ejecuta cada vez, indefinidamente. En createReaction hay dos funciones con papeles distintos: track(fn) establece las dependencias ejecutando fn en un scope de rastreo —pero sin correr tu reacción—, y el callback que pasaste a createReaction es lo que corre cuando esas dependencias se invalidan, exactamente una vez.

Ese “una vez” es la esencia. Tras dispararse, la reacción queda inerte. Los cambios siguientes de dato no hacen nada, porque la suscripción se consumió al notificar. Es una alarma de un disparo, no un sensor continuo.

El caso más limpio de ese comportamiento es marcar un formulario como sucio en la primera edición, y solo en la primera:

const [sucio, setSucio] = createSignal(false);

const track = createReaction(() => setSucio(true));
track(() => campos()); // a la primera tecla, sucio pasa a true y deja de escuchar

En cuanto campos cambie una vez, sucio queda en true y la reacción se apaga sola: seguir observando no aportaría nada, porque ya está sucio para siempre. Con un createEffect tendrías que añadir una guarda —if (sucio()) return— que apague el vigilante a mano; createReaction expresa ese apagado en su propia semántica, sin ruido.

flowchart LR
R[createReaction con callback] --> T[llamas track con una lectura]
T --> W[observa esas fuentes]
W --> C[cambia una fuente]
C --> O[el callback corre una sola vez]
O --> RE[rearmar llamando track de nuevo]
RE --> W
style O fill:#f9e2af,color:#11111b
style W fill:#89b4fa,color:#11111b

Re-armar: de un disparo a los que quieras

Si necesitas observar más de una vez, re-armas la reacción llamando a track otra vez —lo natural es hacerlo desde dentro del propio callback—. Así controlas con exactitud cuántas veces y bajo qué condiciones sigues escuchando.

const track = createReaction(() => {
  console.log("cambió; me re-armo para la próxima");
  track(() => dato()); // vuelve a observar
});

track(() => dato());

Esto reconstruye a mano lo que createEffect hace solo. Y ahí está la enseñanza: createReaction te entrega el mecanismo desnudo para que decidas la política de re-suscripción, en lugar de heredar la política automática. Puedes re-armar siempre, re-armar bajo condición, o cambiar qué observas en cada vuelta pasando una lectura distinta a track.

Cuándo es la herramienta correcta

createReaction es de nicho por diseño. La inmensa mayoría del código quiere createEffect o on. Reserva createReaction para cuando la semántica de un solo disparo es exactamente lo que el problema pide.

✍️

Marcar como sucio

Detectar la primera edición de un formulario para activar el estado sucio y dejar de escuchar: después de la primera tecla, seguir observando no aporta nada.

Trabajo diferido

Retrasar una inicialización costosa hasta la primera vez que un valor cambie, sin pagar el coste de un efecto permanente que vigila para siempre.

🔌

Puentes imperativos

Notificar a una librería externa una única vez ante el siguiente cambio, o construir primitivas propias como un watchOnce sobre esta base.

Construir una primitiva encima

El valor real de createReaction aflora al usarla como cimiento. Como te entrega el rastreo y la reacción por separado, puedes empaquetar políticas de re-suscripción a medida dentro de primitivas reutilizables. Como ejemplo, un alCambiar que ejecuta un callback en cada cambio de una fuente pero nunca en el montaje, re-armándose solo.

import { createReaction } from "solid-js";

function alCambiar<T>(fuente: () => T, efecto: (v: T) => void) {
  const track = createReaction(() => {
    efecto(fuente());        // reacciona
    track(() => fuente());   // se re-arma para el próximo cambio
  });
  track(() => fuente());     // primera observación, sin ejecutar el efecto
}

// uso: reacciona a cada cambio de estado, jamás al valor inicial
alCambiar(estado, (v) => console.log("nuevo estado:", v));

Fíjate en lo que has construido: un on(fuente, efecto, { defer: true }) casero, ensamblado desde sus dos mitades. Podrías variar la política sin tocar el resto —disparar solo las tres primeras veces, esperar a que la fuente cumpla una condición, cambiar de fuente sobre la marcha— con retoques mínimos en el re-armado. Ese es el sentido de una primitiva de bajo nivel: no compite con on ni con createEffect, es la materia con la que se fabrican primitivas como esas. Cuando entiendes que las abstracciones cómodas del día a día se construyen con este ladrillo, dejas de verlo como una curiosidad y empiezas a verlo como el fondo del arsenal.

ℹ️
createReaction frente a on con defer

Se confunden porque ambas ignoran el estado inicial, pero difieren en lo esencial. on(fuente, fn, { defer: true }) salta el run del montaje y luego reacciona a cada cambio, para siempre: es un watcher permanente que empieza callado. createReaction reacciona una sola vez y muere salvo que la re-armes: es un disparo único. Si quieres vigilar de forma continua pero sin ejecutar al inicio, es on con defer. Si quieres capturar solo el próximo cambio y desentenderte, es createReaction.

📝
Vive en el árbol de ownership, como todo lo demás

createReaction no escapa a las reglas de disposición de Solid. La observación que estableces con track pertenece al dueño reactivo donde se creó —un componente, un createRoot—, y se limpia cuando ese dueño se destruye. No hay que desregistrar nada a mano: si el componente se desmonta antes de que la reacción se dispare, la suscripción muere con él. Esto la hace segura para usar dentro de componentes sin temor a fugas, igual que un efecto.

Toda computación reactiva es la misma máquina; createReaction te la entrega desmontada

En la lección de variantes de timing viste que memos, computeds, render effects y efectos son un mismo nodo con horarios distintos. createReaction revela otra cara de esa misma unidad, y quizá la más instructiva, porque descompone el nodo reactivo en sus dos operaciones atómicas y te las pone en las manos por separado. Una computación reactiva, en el fondo, hace dos cosas: rastrea —ejecuta código en un scope que anota qué signals se leen y teje suscripciones— y responde —cuando esas suscripciones se invalidan, corre algo—. En createEffect las dos van fundidas e inseparables: el cuerpo es a la vez lo que rastrea y lo que responde, y la re-suscripción es automática porque cada respuesta vuelve a ejecutar el cuerpo que rastrea. createReaction corta esa fusión. track(fn) es puro rastreo: corre fn para descubrir dependencias, pero no es tu respuesta. El callback es pura respuesta: corre al invalidarse, pero no rastrea nada ni se re-suscribe. Al separarlas, dos decisiones que en un efecto están soldadas se vuelven tuyas: qué observas —puedes cambiarlo en cada re-armado, pasando una lectura distinta— y cuántas veces respondes —una, condicional, o infinitas si re-armas siempre—. Esa es la razón de que createReaction sea el ladrillo con el que se construyen primitivas de más alto nivel: es reactividad antes de que Solid tome por ti las decisiones de política que hacen cómodo el día a día. No la uses por sofisticación —un efecto es más claro y casi siempre correcto—, sino cuando necesites justamente lo que la fusión automática te niega: capturar el próximo cambio y solo ese, o gobernar a mano la re-suscripción que las demás primitivas dan por hecha. Entenderla no cambia cómo escribes la mayoría de tu código, pero cambia cómo piensas todo el resto: ves los efectos y los on no como cajas negras, sino como este mismo mecanismo con la política ya cableada, y comprendes al fin que la reactividad de Solid no es un catálogo de funciones, sino una sola máquina que puedes usar montada o, cuando el problema lo exige, desmontar pieza a pieza.

⚔️ Domina el disparo único
  1. Crea una createReaction, obsérvala con track(() => dato()) y confirma que el callback corre solo en el primer cambio, no en los siguientes.
  2. Re-arma la reacción desde dentro del callback y verifica que ahora responde a cada cambio, como un efecto hecho a mano.
  3. Implementa un “marcar como sucio”: dispara una sola vez ante la primera edición de un signal y comprueba que no vuelve a hacerlo.
  4. Compara lado a lado un on(x, fn, { defer: true }) y una createReaction sobre x: cambia x dos veces y observa cuál reacciona una vez y cuál dos.
  5. Cambia qué observas entre un disparo y el siguiente pasando una lectura distinta a track en el re-armado, y razona por qué eso sería imposible con un createEffect.