wandres.dev
SETUP Y JSX DE SOLID · no es React

Tu primer componente reactivo: el contador

Un contador construido paso a paso con createSignal, y la explicación exacta de por qué reacciona: el componente corre una vez, el getter count() leído dentro del JSX se suscribe a la señal, y el setter dispara el efecto de render que actualiza solo ese nodo de texto.

⏱ 13 min

Un contador es el “hola mundo” de la reactividad, y en Solid es el mejor lugar para ver el mecanismo completo funcionando sobre el JSX que ya conoces. No hay estado que gestionar ni renders que optimizar: hay una señal, una lectura que se suscribe sola y una escritura que despierta exactamente al nodo que depende de ella. Vamos a construirlo línea a línea y, sobre todo, a entender por qué reacciona.

🎯 Al terminar esta lección sabrás
  • Construir un contador con createSignal entendiendo su par getter/setter.
  • Ver por qué el cuerpo del componente se ejecuta una sola vez.
  • Explicar la suscripción: cómo count() dentro del JSX rastrea la señal.
  • Trazar la propagación desde el setter hasta la actualización quirúrgica del DOM.

El contador, línea a línea

createSignal es el primitivo fundacional de Solid. Recibe un valor inicial y devuelve una tupla de dos funciones: un getter que lee el valor —y, si estás dentro de un contexto reactivo, se suscribe a él— y un setter que lo cambia y notifica a quien lo observaba.

import { createSignal } from "solid-js";

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

  return (
    <button onClick={() => setCount(count() + 1)}>
      Has pulsado {count()} veces
    </button>
  );
}

Fíjate en que count es una función: se lee invocándola, count(), no accediendo a una variable. Esa llamada no es un capricho de sintaxis; es la que permite el rastreo. Y setCount acepta tanto un valor nuevo como una función que recibe el valor previo, forma que conviene cuando la actualización depende del estado anterior.

// dos formas de incrementar; la segunda es mas segura ante concurrencia
setCount(count() + 1);
setCount((n) => n + 1);

La tupla es solo una convención: createSignal devuelve un array de dos posiciones que desestructuras con los nombres que quieras. Nada te obliga a llamarlos count y setCount; lo esencial es el par lectura/escritura. Separarlos —en vez de exponer un objeto mutable— es precisamente lo que permite a Solid saber con exactitud cuándo lees y cuándo escribes.

El componente corre una vez

Aquí está la idea que lo cambia todo, y la que más cuesta a quien viene de React. El cuerpo de Contador —la línea del createSignal, el return— se ejecuta una sola vez, cuando el componente se crea. Nunca se vuelve a llamar. No hay ciclo de render, no hay reejecución de la función en cada cambio.

¿Cómo cambia entonces el número en pantalla si la función no se repite? Porque lo que reacciona no es el componente, sino un fragmento diminuto que el compilador extrajo del JSX. Recuerda lo que genera Solid: la parte estática del botón se hornea en una plantilla, y {count()} se convierte en un insert() que crea un efecto de render. Ese efecto —y solo ese— es lo que vuelve a correr cuando la señal cambia.

flowchart LR
A[Contador se ejecuta una vez] --> B[crea la senal count con valor 0]
B --> C[el JSX liga count dentro de un efecto de render]
C --> D[el efecto lee count y se suscribe]
D --> E[nodo de texto muestra 0]
ℹ️
Una vez el componente, muchas veces el efecto

Separa dos tiempos distintos. El componente es una fábrica que se ejecuta una vez para montar el andamiaje: crea señales, describe el JSX, registra efectos. Los efectos son los obreros que vuelven al trabajo cada vez que su material —una señal— cambia. Confundir ambos es la raíz de casi todo malentendido inicial con Solid. No preguntes “cuándo se re-renderiza el componente” —nunca—, pregunta “qué efecto observa esta señal”, porque ese, y no el componente, es quien reacciona.

Por qué reacciona: la suscripción

El mecanismo es una danza de dos pasos entre lectura y escritura, sin magia y sin diffing.

La lectura suscribe. Cuando el efecto de render que envuelve {count()} se ejecuta por primera vez, invoca count(). En ese instante, Solid tiene un efecto “activo” apuntado en una variable global interna, y el getter, al ejecutarse, mira ahí y se anota: “este efecto depende de mí”. Así se construye la arista del grafo, automáticamente, por el mero hecho de leer dentro de un contexto de rastreo. No declaras dependencias en una lista; la lectura es la declaración.

La escritura notifica. Cuando pulsas el botón y setCount corre, la señal compara el valor nuevo con el viejo; si difieren, recorre su lista de suscriptores y los marca para reejecutarse. El efecto de render vuelve a correr, invoca count() de nuevo —ahora devuelve el valor incrementado— y actualiza el nodo de texto. Nada más se toca: ni el <button>, ni el texto fijo “Has pulsado”, ni el resto de la página.

📖

Leer = suscribirse

Invocar count() dentro de un efecto crea la dependencia. Fuera de todo efecto, la misma llamada solo lee el valor sin suscribir a nadie.

✍️

Escribir = notificar

setCount compara por igualdad, y si hay cambio despierta a los suscriptores. Un set con el mismo valor no propaga nada.

🎯

Actualización mínima

Solo se reejecuta el efecto que leyó la señal. El resto del DOM permanece intacto: no hay árbol que recomparar.

🧮

Derivar es gratis

Una función como () => count() * 2 reusada en el JSX se resuscribe sola; para memoizar cálculos caros existe createMemo.

Un corolario importante: la suscripción solo ocurre si lees la señal dentro de un contexto de rastreo —un efecto de render, un createEffect, un createMemo—. La misma llamada count() en el cuerpo del componente, que corre una vez y fuera de todo efecto, lee el valor pero no suscribe a nadie. Por eso el número del JSX reacciona mientras que una copia guardada en una variable al montar se queda congelada: no importa dónde vive el valor, importa desde dónde lo lees.

Y una precisión sobre la escritura: si cambias varias señales de golpe en un mismo manejador, Solid agrupa las notificaciones y ejecuta cada efecto afectado una sola vez al final, no una por cada set. Esa agrupación —el batching— evita estados intermedios visibles y trabajo redundante, y en el Solid actual sucede de forma automática dentro de los manejadores de eventos.

Este último punto merece una demostración. Un valor derivado es simplemente una función que lee la señal; puedes usarla en el JSX y reaccionará igual, porque hereda la suscripción de la señal que consume.

function Contador() {
  const [count, setCount] = createSignal(0);
  const doble = () => count() * 2;          // derivado: solo una funcion

  return (
    <div>
      <button onClick={() => setCount((n) => n + 1)}>+1</button>
      <p>Valor {count()} y su doble {doble()}</p>
    </div>
  );
}

¿Cuándo conviene createMemo en vez de una función derivada? Solo cuando el cálculo es caro o lo consumen varios lugares: el memo cachea su resultado y recomputa únicamente si sus dependencias cambian, mientras que la función derivada se reevalúa en cada lectura. Para un doble trivial el memo sobra; para filtrar y ordenar una lista grande leída en tres sitios, paga con creces.

Contra el reflejo de React

Poner los dos modelos lado a lado revela filosofías opuestas bajo el mismo contador. En React, pulsar el botón llama a setCount y eso reejecuta la función entera del componente: se fabrica un nuevo árbol virtual, se compara con el previo y se parchea la diferencia. El componente es una función del estado que se evalúa una y otra vez.

En Solid no sucede nada de eso. setCount no vuelve a llamar a Contador —su cuerpo corrió una vez y no volverá—; solo notifica al efecto de render que leyó la señal. No se crea ningún elemento, no se compara ningún árbol, no se reevalúa el <button> ni el texto fijo. De ahí que en Solid no existan ni hagan falta useMemo, useCallback ni los arrays de dependencias: no hay reejecución de la que defenderse, así que no hay nada que memoizar por precaución.

// anti-patron: la condicion se evalua una sola vez, al montar
function Mal(props) {
  if (props.n > 10) return <p>Muchos</p>;   // no reacciona a props.n
  return <p>Pocos</p>;
}

// correcto: la lectura reactiva vive DENTRO del JSX
function Bien(props) {
  return <p>{props.n > 10 ? "Muchos" : "Pocos"}</p>;
}

La regla de oro que se deduce: en Solid la reactividad vive dentro del JSX y de los efectos, donde las lecturas quedan rastreadas, no en el cuerpo del componente, que corre una vez. Un if en la raíz decide para siempre; la misma condición dentro del JSX se reevalúa cada vez que su señal cambia.

El valor no vive en una variable: vive en el grafo

El salto conceptual definitivo es dejar de pensar en count como “una variable que guarda un número” y empezar a verlo como “un nodo de un grafo de dependencias que emite valores en el tiempo”. En el modelo imperativo clásico, un número es un dato inerte en una casilla de memoria; alguien lo lee cuando quiere y nadie se entera si cambia. En Solid, leer una señal deja rastro: quien la lee dentro de un efecto queda enganchado a ella, de modo que la señal sabe en todo momento quién depende de su valor. Escribir, entonces, no es solo cambiar una casilla; es enviar una onda por todas las aristas que salen de ese nodo, despertando exactamente a los efectos suscritos y a nadie más. Por eso Solid no necesita un virtual DOM que compare, ni listas de dependencias que mantengas a mano, ni la disciplina de envolver funciones en memos defensivos: el grafo ya sabe qué depende de qué, porque lo aprendió leyendo. Cuando esto encaja, un contador deja de parecerte trivial. Es la demostración más pequeña posible de una propiedad enorme: en Solid, el estado y sus consumidores están conectados por construcción, y la propagación del cambio es un hecho estructural, no un algoritmo que se ejecuta a posteriori. Todo lo demás que aprenderás —memos, stores, resources, efectos— son variaciones sobre este mismo grafo. El contador no es el principio del camino: es el camino entero, en miniatura.

⚔️ Disecciona tu contador
  1. Escribe el Contador con createSignal, móntalo con render y confirma que incrementa al pulsar.
  2. Añade un console.log como primera línea del cuerpo del componente y otro dentro de un createEffect(() => console.log(count())); pulsa varias veces y observa cuál se imprime una vez y cuál en cada clic.
  3. Cambia setCount(count() + 1) por la forma funcional setCount((n) => n + 1) y razona por qué es más robusta.
  4. Crea un derivado const doble = () => count() * 2, úsalo en el JSX y verifica que reacciona sin declararlo como señal.
  5. Llama a setCount(count()) —el mismo valor— y comprueba con el efecto que no se dispara ninguna reejecución.