wandres.dev
CREATESTORE · reactividad anidada

Grano fino en stores: solo se actualiza quien lee lo que cambió

El suscriptor de un store no es el componente sino la expresión concreta del DOM que lee la hoja. Por eso cambiar estado.usuario.nombre no re-renderiza ningún componente: reejecuta únicamente el binding ligado a esa hoja y toca solo ese nodo del DOM. Diez componentes que leen diez campos distintos se actualizan de forma independiente, y la topología del árbol de componentes es irrelevante para lo que se repinta.

⏱ 14 min

En React, cambiar un campo de estado re-renderiza el componente y, salvo que optimices, a sus hijos. En Solid con un store ocurre algo cualitativamente distinto: no se re-renderiza nada. Cambiar estado.usuario.nombre no despierta al componente que lo pinta, sino a la expresión exacta del DOM que lo lee. Si diez componentes leen diez campos distintos del mismo store y cambias uno, solo la porción de DOM ligada a ese campo se actualiza; los otros nueve ni se enteran. Entender por qué exige mirar quién es, en realidad, el suscriptor.

🎯 Al terminar esta lección sabrás
  • Ver que el suscriptor de un store no es el componente, sino la expresión que lee la hoja.
  • Entender por qué cambiar una propiedad no re-renderiza ningún componente.
  • Comprobar que componentes hermanos que leen hojas distintas se actualizan por separado.
  • Razonar el grano fino como una propiedad del grafo, no una optimización opcional.

Quién lee es quien se actualiza

Cuando el compilador de Solid encuentra una lectura dinámica en el JSX —{estado.usuario.nombre}— la envuelve en un pequeño cómputo cuyo único trabajo es mantener actualizado ese nodo del DOM. Ese cómputo es el que lee la hoja, y por tanto es el que se suscribe a ella. La granularidad de la suscripción no es el componente ni la función: es el binding, la expresión concreta que produce un pedazo de DOM.

function Panel() {
  return (
    <>
      <h1>{estado.usuario.nombre}</h1>   {/* binding suscrito a usuario.nombre */}
      <p>{estado.usuario.edad}</p>        {/* binding suscrito a usuario.edad */}
    </>
  );
}

setEstado("usuario", "edad", 37); // solo el <p> se reevalúa; el <h1> ni se toca

Cada expresión dinámica tiene su propio cómputo, su propia lista de dependencias y su propio nodo del DOM que actualizar. Por eso decir el componente se actualiza es, en Solid, una imprecisión: el componente no se actualiza jamás. Lo que se actualiza es una expresión, y solo si la hoja que lee ha cambiado.

El giro mental que esto exige es dejar de preguntar qué componente debe actualizarse y empezar a preguntar qué expresiones leen esta hoja. Cambia el sujeto de la frase: no es el componente el que reacciona, son sus lecturas individuales. Un mismo componente puede alojar una expresión que se reejecuta mil veces por segundo junto a otra que no se toca en toda la sesión, y ambas conviven sin interferir, porque cada una es su propio suscriptor con su propia lista de dependencias.

No se re-renderiza el componente, se reejecuta la expresión

El cuerpo del componente corre una sola vez, al crearse. No hay ciclo de render, no hay reconciliación de árboles, no hay comparación de props. Lo único que vuelve a ejecutarse tras un cambio son los cómputos que el compilador tejió alrededor de cada expresión dinámica. Un cambio de estado no reinvoca la función del componente; propaga por el grafo hasta los bindings que dependen de la hoja tocada, y ahí se detiene.

flowchart LR
A[hoja usuario.nombre] --> B[binding del h1]
C[hoja usuario.edad] --> D[binding del p]
B --> E[nodo de texto del h1]
D --> F[nodo de texto del p]
G[setEstado usuario edad 37] --> C
style C fill:#f38ba8,color:#11111b
style D fill:#f38ba8,color:#11111b
style F fill:#f38ba8,color:#11111b
style A fill:#45475a,color:#cdd6f4

El diagrama hace visible la cadena: setEstado marca sucia la hoja edad, esa hoja solo tiene como oyente al binding del <p>, y ese binding solo escribe en un nodo de texto. La rama de nombre —hoja, binding y nodo— permanece intacta porque nada la tocó. No es que Solid decida no actualizarla tras comparar; es que nunca la incluyó en la propagación, porque no dependía del cambio.

Esta es la diferencia de fondo con un modelo de virtual DOM. Allí, un cambio de estado dispara un nuevo render de un subárbol de componentes, y un algoritmo de diffing decide después qué del DOM tocar. En Solid no hay render nuevo ni diffing: el grafo ya sabe, por construcción, qué bindings dependen de qué hojas, y el cambio viaja directo por esas aristas. El trabajo es proporcional a lo que cambió, no al tamaño del componente que lo contiene.

ℹ️
El grano fino sobrevive a través de derivaciones

Si en vez de leer la hoja directamente en el JSX la lees a través de una función derivada o un createMemo, la cadena de suscripción se conserva: el memo depende de la hoja, el binding depende del memo, y cambiar la hoja propaga por ambos hasta el mismo nodo del DOM. Interponer derivaciones no rompe el grano fino, lo encadena. Lo único que lo pierde es leer la rama entera o destructurar, porque ahí dejas de nombrar la hoja concreta.

Grano fino a través de componentes anidados

Lo más contraintuitivo para quien llega de React es que la frontera de un componente no crea ninguna barrera de actualización. Puedes partir la interfaz en tantos componentes como quieras; cada uno que lea una hoja del store se suscribe a ella por su cuenta, y cambiar esa hoja actualiza solo al que la lee, sin cascada por el árbol.

function Nombre() {
  return <span>{estado.usuario.nombre}</span>;
}
function Edad() {
  return <span>{estado.usuario.edad}</span>;
}

// Ambos leen del mismo store, pero hojas distintas:
setEstado("usuario", "edad", 37); // actualiza solo <Edad/>, no <Nombre/>

Puedes volverlo tangible instrumentando cada lectura con un contador. Al cambiar una hoja verás incrementarse solo el contador del cómputo que la lee, mientras el otro permanece congelado —la prueba empírica de que la suscripción es por hoja y no por componente:

import { createEffect } from "solid-js";

createEffect(() => { estado.usuario.nombre; console.count("lee nombre"); });
createEffect(() => { estado.usuario.edad; console.count("lee edad"); });

setEstado("usuario", "edad", 40); // incrementa solo el contador "lee edad"

Como la suscripción es por hoja, la topología de componentes es irrelevante para lo que se repinta. Da igual que Nombre y Edad sean hermanos, primos o estén a diez niveles de anidamiento uno del otro: lo único que determina qué se actualiza es qué hoja lee cada quién. No hay que memoizar componentes, ni cortar cascadas con memo, ni preocuparse por que un padre repinte a sus hijos, porque nada de eso ocurre.

🔌

El binding es el suscriptor

Cada expresión dinámica del JSX es su propio cómputo con su nodo del DOM. Suscribe a las hojas que lee y se reejecuta solo si una de ellas cambia.

1️⃣

El componente corre una vez

El cuerpo se ejecuta al crearse y no vuelve. Tejer el grafo es su único trabajo; la vida ocurre después en las aristas, no en repeticiones de la función.

Por qué desaparecen memo y las listas de dependencias

En un modelo de renders, buena parte del código de optimización existe para contener el trabajo que el propio modelo genera: memoizar componentes para que no se repinten cuando su padre lo hace, envolver callbacks para que no cambien de identidad, declarar listas de dependencias para acotar cuándo corre un efecto. En Solid con un store, ese código no se optimiza: sobra, porque el problema que resolvía no existe.

// No hay padre que repinte al hijo: no hay nada que memoizar
function Lista() {
  return <For each={estado.filas}>{(fila) => <Fila dato={fila} />}</For>;
}
// Cambiar fila.n actualiza solo el binding de esa fila; ni Lista ni las demás Fila corren

El tracking automático sustituye a las listas de dependencias: un binding depende exactamente de las hojas que leyó en su última ejecución, ni una más ni una menos, y esa lista se recalcula sola en cada corrida. No hay un array de dependencias que mantener sincronizado a mano, ni el error clásico de olvidar una y quedarse con un valor rancio. El grafo es la lista de dependencias, y siempre está al día.

💡
No busques el componente que se re-renderiza: no existe

Cuando depures rendimiento en Solid, abandona el reflejo de preguntar qué componente se repinta de más, porque la respuesta siempre es ninguno. La pregunta útil es otra: qué binding se reejecuta y qué hoja lo dispara. Un console.log dentro de la expresión sospechosa —no en el cuerpo del componente— es lo que te dice la verdad. Si algo se actualiza más de lo que esperas, casi siempre es que leíste una rama demasiado arriba del árbol y te suscribiste a más de lo que creías.

⚠️
Leer el objeto entero te suscribe a su rama, no a sus hojas

Si en vez de estado.usuario.nombre lees estado.usuario como un todo —lo pasas a JSON.stringify, lo esparces, lo recorres entero— dejas de suscribirte a una hoja y pasas a depender de la rama completa, de modo que cualquier cambio dentro de ella te despierta. El grano fino vive en leer hasta la hoja concreta: cuanto más arriba del árbol te detienes, más grueso es tu grano. Y destructurar const { nombre } = estado.usuario es aún peor, porque congela el valor en ese instante y rompe la reactividad por completo, igual que destructurar props.

El grano fino no es una optimización: es la arquitectura del store

La tentación, viniendo de un modelo de renders, es leer todo esto como una lista de optimizaciones —Solid evita repintar de más, salta componentes, no hace diffing— y archivarlo como rendimiento. Es un error de encuadre que te impedirá razonar bien. En Solid el grano fino no es algo que el sistema logre a pesar de re-renderizar; es que no existe el re-render en absoluto. El modelo mental correcto es un grafo de dependencias donde las fuentes son las hojas del store y los sumideros son los bindings del DOM, y donde un cambio no es un evento que dispara un recálculo global, sino una señal que viaja por aristas concretas hasta exactamente los nodos que dependían de la fuente que cambió. Cuando interiorizas esto, muchas preguntas heredadas se disuelven: no preguntas cómo evito que este componente se re-renderice de más, porque no se renderiza; no preguntas dónde pongo el memo para cortar la cascada, porque no hay cascada; no preguntas por qué mi hijo se repinta si sus props no cambiaron, porque los hijos no se repintan. La unidad de reactividad no es el componente, es la lectura de una hoja, y la unidad de actualización no es el subárbol, es el binding. El componente pasa a ser lo que siempre debió ser —una función que se ejecuta una vez para tejer el grafo y describir el DOM— y toda la vida de la aplicación transcurre en las aristas de ese grafo, no en repeticiones de la función. Esa inversión es la que hace que un store escale a estados grandes sin degradarse: el coste de un cambio depende de cuántos bindings leen la hoja tocada, no de cuán grande es el estado ni cuán profundo es el árbol de componentes.

⚔️ Demuestra que el componente no se re-renderiza
  1. Pinta dos campos del mismo store en dos elementos y pon un console.log en cada expresión derivada; cambia una hoja y confirma que solo se reevalúa una.
  2. Añade un console.log en el cuerpo del componente y verifica que no vuelve a ejecutarse nunca tras un cambio de estado.
  3. Divide la UI en dos componentes hijos que lean hojas distintas del mismo store; cambia una hoja y confirma que solo un hijo se actualiza.
  4. Destructura una hoja con const { x } = estado.rama, úsala en el JSX y observa cómo deja de reaccionar; razona por qué.
  5. Explica por qué la topología del árbol de componentes es irrelevante para lo que se actualiza en un store.