Saltar al contenido

Crear tus propios primitivos reactivos

El patron canonico para fabricar primitivos reactivos reutilizables que envuelven una fuente externa: un signal interno que hace de espejo, una suscripcion a la fuente que lo escribe, un onCleanup que corta la suscripcion atado al dueno, y una API que devuelve solo el getter cuando la autoridad del valor es externa. Por que un accessor de solo lectura comunica intencion, por que el prefijo create anuncia ciclo de vida, y como hacer el primitivo reactivo a sus propios argumentos re-suscribiendose dentro de un createEffect con onCleanup anidado.

El cuarteto: signal, suscripción, limpieza, getter

Un primitivo reactivo que envuelve una fuente externa se construye siempre igual. Creas un signal interno que hará de espejo del valor de la fuente; te suscribes a la fuente para escribir ese signal en cada cambio; registras un onCleanup que deshace la suscripción cuando el dueño muere; y devuelves solo el getter. El ejemplo canónico es una consulta de medios reactiva:

TSX
import { createSignal, onCleanup, type Accessor } from "solid-js";function createMediaQuery(consulta: string): Accessor<boolean> {  const mql = window.matchMedia(consulta);  const [coincide, setCoincide] = createSignal(mql.matches); // signal interno = espejo  const onChange = () => setCoincide(mql.matches);           // la fuente lo escribe  mql.addEventListener("change", onChange);  onCleanup(() => mql.removeEventListener("change", onChange)); // corta al morir el dueno  return coincide;                                            // API: solo el getter}

El mismo esqueleto sirve para envolver un setInterval, un ResizeObserver, la posición del ratón o el estado de la conexión. Cambia la fuente y su forma de suscribirse; el cuarteto no se mueve.

TSX
function createTick(ms: number): Accessor<number> {  const [n, setN] = createSignal(0);  const id = setInterval(() => setN((x) => x + 1), ms);  onCleanup(() => clearInterval(id));  return n;}

Solo el getter: la API comunica quién manda

La decisión de diseño más reveladora es qué devuelves. Cuando el valor lo gobierna una fuente externa —el navegador decide si la media query coincide, el reloj decide el tick—, el consumidor no tiene ninguna autoridad para escribirlo, así que exponer un setter sería mentir sobre el contrato. Por eso estos primitivos devuelven un Accessor<T> desnudo, de solo lectura. La firma se convierte en documentación: quien la lee entiende de un vistazo que puede observar el valor pero no fijarlo.

El contraste aclara la regla. Un primitivo como createStoredSignal —que viste en el nivel de disposición— devuelve la tupla [get, set] porque ahí el consumidor sí es dueño de las escrituras: él decide el tema, y el primitivo se limita a persistirlo. La heurística es nítida: devuelve una tupla cuando el consumidor manda sobre el valor; devuelve solo el getter cuando manda una fuente de fuera. La forma de la API codifica la dirección de la autoridad.

Reactivo a sus propios argumentos

El createMediaQuery de arriba tiene un límite: fija la consulta una vez. Un primitivo maduro acepta que sus entradas también cambien —que la consulta llegue como un accessor y varíe en el tiempo—. La técnica es envolver la suscripción en un createEffect y poner el onCleanup dentro del efecto. Ahí está la joya del sistema de dueños: un onCleanup anidado en un efecto corre antes de cada re-ejecución y también al disponerse, de modo que la re-suscripción es automática y no filtra nada.

TSX
import { createSignal, createEffect, onCleanup, type Accessor } from "solid-js";function createMediaQuery(consulta: Accessor<string> | string): Accessor<boolean> {  const leer = typeof consulta === "function" ? consulta : () => consulta;  const [coincide, setCoincide] = createSignal(false);  createEffect(() => {    const mql = window.matchMedia(leer());        // re-lee si la consulta cambia    setCoincide(mql.matches);    const onChange = () => setCoincide(mql.matches);    mql.addEventListener("change", onChange);    onCleanup(() => mql.removeEventListener("change", onChange)); // corre en cada vuelta  });  return coincide;}

Cuando la consulta cambia, el efecto se re-ejecuta: su onCleanup retira el listener viejo, el cuerpo se suscribe a la media query nueva, y el signal espejo se actualiza. Un solo mecanismo —el efecto con su limpieza anidada— cubre a la vez el cambio de argumento y la baja final. Este es el patrón que, escalado, sostiene toda la colección oficial de primitivos.

Enriquecer la API y componer primitivos

El getter desnudo es el caso más simple, pero un primitivo puede devolver una forma más rica cuando el valor lo pide. Una posición de ratón son dos números que cambian juntos, así que su primitivo natural devuelve un accessor a un objeto { x, y }, no dos accessores sueltos que se actualicen a destiempo.

TSX
import { createSignal, onCleanup, type Accessor } from "solid-js";function createMousePosition(): Accessor<{ x: number; y: number }> {  const [pos, setPos] = createSignal({ x: 0, y: 0 });  const onMove = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });  window.addEventListener("mousemove", onMove);  onCleanup(() => window.removeEventListener("mousemove", onMove));  return pos;}

Y como un primitivo no es más que una función, compone: uno puede envolver el accessor de otro para apilarle una política, igual que compones funciones puras. Un createDebounced toma cualquier accessor y devuelve otro que se retrasa, sin saber ni importarle de dónde salió el original.

TSX
import { createSignal, createEffect, onCleanup, type Accessor } from "solid-js";function createDebounced<T>(fuente: Accessor<T>, ms: number): Accessor<T> {  const [eco, setEco] = createSignal(fuente());  createEffect(() => {    const v = fuente();                       // rastrea la fuente    const id = setTimeout(() => setEco(() => v), ms);    onCleanup(() => clearTimeout(id));        // cancela el pendiente en cada cambio  });  return eco;}// apila politicas: la posicion del raton, pero antirebotadaconst posLenta = createDebounced(createMousePosition(), 100);

Cada capa añade una política y delega el resto en la de abajo. Esa capacidad de encadenar primitivos como quien encadena funciones es lo que convierte cuatro líneas de patrón en un lenguaje entero, y su único límite es tu imaginación, no la librería.