Qué es un memo: una derivación cacheada
createMemo produce un valor derivado que Solid guarda en caché y solo recalcula cuando cambian sus fuentes. Un memo es, a la vez, un efecto que observa y un signal que otros observan: el nodo intermedio del grafo reactivo.
Un signal guarda un valor que cambia; un efecto reacciona a esos cambios. En medio falta una pieza: el valor que se calcula a partir de otros y que quieres reutilizar sin recomputarlo cada vez. Ese es el trabajo de createMemo: una derivación con memoria, que recuerda su último resultado y solo lo recalcula cuando alguna de sus fuentes cambia de verdad.
- Entender la firma de
createMemoy qué devuelve. - Comprender la doble naturaleza del memo: observador y observable.
- Seguir el ciclo de una recomputación: marcar, recomputar, comparar, notificar.
- Ver por qué el memo se ejecuta una vez por cambio, no por lectura.
La firma: una derivación con memoria
createMemo recibe una función de cálculo y devuelve un accesor de solo lectura —una función que, al invocarla, entrega el valor cacheado—:
import { createSignal, createMemo } from "solid-js";
const [radio, setRadio] = createSignal(2);
const area = createMemo(() => {
console.log("calculando area");
return Math.PI * radio() ** 2;
});
area(); // lee el valor cacheado; NO recalcula
area(); // idem: "calculando area" se imprimió una sola vez
setRadio(3); // ahora sí: la fuente cambió, el memo se recomputa
El accesor area no tiene setter: nadie escribe un memo, su valor se deriva. Esa asimetría es deliberada —un memo es una consecuencia, no una fuente de la verdad— y garantiza un flujo de datos en una sola dirección: de las fuentes hacia los consumidores, nunca al revés. La función recibe además el valor anterior, útil para acumulaciones y transiciones:
const maximo = createMemo((prev: number) => Math.max(prev, radio()), 0);
El segundo argumento (0) es el valor inicial que se pasa como prev en la primera ejecución. Un tercer argumento opcional acepta equals y name; verás equals a fondo en la lección 4. Y un detalle clave: el memo es eager, corre una vez al crearse para tener ya un valor, y desde ahí se recomputa cuando cambian sus fuentes, no cuando lo lees.
El memo devuelve un Accessor<T>, exactamente la misma firma que el getter de un signal: una función sin argumentos que devuelve T.
import type { Accessor } from "solid-js";
const areaTipada: Accessor<number> = createMemo(() => Math.PI * radio() ** 2);
Que compartan tipo no es casual. Desde fuera, un memo y un getter de signal son indistinguibles: ambos son funciones que lees para obtener un valor rastreable. Por eso una API que espera un Accessor acepta cualquiera de los dos, y puedes pasar signals, memos o funciones derivadas de forma intercambiable por todo tu código sin que el consumidor sepa —ni le importe— cuál recibió.
Doble naturaleza: efecto y signal a la vez
Aquí está la idea que lo explica todo. Internamente, un memo es dos cosas fundidas en un mismo nodo:
Es un efecto (observa)
Como un createEffect, el memo rastrea los signals que lee dentro de su función y se suscribe a ellos. Cuando una fuente cambia, el nodo se marca sucio y se agenda para recomputar.
Es un signal (se observa)
Como un createSignal, el memo es un valor observable: los efectos y memos que lo leen se suscriben a él y reaccionan cuando su resultado cambia.
Por eso el memo ocupa el centro del grafo. Un efecto es una hoja: solo consume, no produce nada observable. Un signal es una raíz: solo produce, no depende de nadie. El memo es el único primitivo que hace las dos cosas, y esa dualidad es lo que permite encadenar derivaciones sobre derivaciones sin escribir una sola línea de orquestación.
flowchart LR S1[signal radio] --> M[memo area] M --> E1[effect render] M --> E2[effect log] M --> M2[memo diametro] style S1 fill:#89b4fa,color:#11111b style M fill:#cba6f7,color:#11111b style M2 fill:#cba6f7,color:#11111b style E1 fill:#a6e3a1,color:#11111b style E2 fill:#a6e3a1,color:#11111b
Y una consecuencia que conviene fijar desde ya: un memo calcula, no actúa. A diferencia de un efecto —cuyo oficio es producir efectos secundarios: tocar el DOM, lanzar una petición, escribir un log—, la función de un memo debe ser pura: entran unas fuentes, sale un valor, sin tocar nada de fuera. Si dentro de un memo disparas un efecto secundario, rompes esa pureza y el cacheo deja de significar nada, porque tu efecto solo correría cuando el memo decide recomputar, de forma impredecible. Regla nemotécnica: efectos en createEffect, cálculos en createMemo.
El ciclo de una recomputación
Ponerle nombre a las fases aclara todo el nivel. Cuando escribes en una fuente del memo, ocurre esto, en orden:
- Marcado. El signal notifica a sus observadores; el memo se marca sucio —pendiente de recalcular—, pero todavía no corre.
- Recomputo. En la fase de actualización, el cuerpo del memo se ejecuta con las fuentes ya frescas y produce un nuevo valor.
- Comparación. El nuevo valor se compara con el anterior mediante
equals; volverás a este paso, decisivo, en la lección 4. - Notificación. Solo si el valor cambió, el memo despierta a sus propios observadores y la onda continúa aguas abajo.
const [t, setT] = createSignal(0);
const par = createMemo(() => t() % 2 === 0);
// setT(2): t marca a par -> par recomputa (true) -> igual al previo -> NO notifica
// setT(3): t marca a par -> par recomputa (false) -> cambió -> notifica
Que el recomputo sea perezoso respecto a la lectura pero atento al cambio es justo lo que separa al memo de una función normal: la función corre al leerse; el memo corre al cambiar sus fuentes y guarda el resultado para que leerlo salga gratis. Y cuando varias fuentes cambian a la vez —dentro de un manejador de eventos, que agrupa las escrituras—, este ciclo se ejecuta una sola vez con todos los valores nuevos, no uno por escritura. Ese agrupamiento, el batching, lo verás al componer memos en la lección 3.
Y una advertencia que anticipa la lección 5: en el núcleo clásico, un memo se recomputa al cambiar sus fuentes aunque nadie lo lea —es el precio de ser eager—. Otra razón más para no crear memos que ningún consumidor necesita.
Cacheo: una ejecución por cambio, no por lectura
La diferencia con una función normal es el cacheo. Si area fuese () => Math.PI * radio() ** 2, cada llamada recomputaría desde cero. Como memo, mil lecturas comparten un único cálculo hasta que radio cambie:
// Tres consumidores, un solo cálculo:
<p>{area()}</p>
<p>{area().toFixed(2)}</p>
createEffect(() => log(area()));
// "calculando area" se imprime UNA vez, no tres.
El memo brilla especialmente cuando lo comparten varios consumidores o cuando otros memos derivan de él. Un valor caro calculado una vez y reutilizado por media docena de vistas, efectos y memos aguas abajo es el caso de uso canónico:
const informe = createMemo(() => resumir(filas())); // caro, una sola vez
const total = createMemo(() => informe().total);
const media = createMemo(() => informe().suma / informe().n);
// total y media reúsan el MISMO informe cacheado; no lo recalculan.
Sin el memo informe, cada uno de esos derivados recomputaría el resumen entero por su cuenta: tres resúmenes caros donde bastaba uno. El memo compartido es, literalmente, la diferencia entre hacer un trabajo por lector y hacer uno solo para todos.
El memo garantiza además que, ante un cambio, se ejecuta exactamente una vez —ni cero, quedaría obsoleto, ni dos, sería derroche—. Esa garantía de “una ejecución por cambio” es la base sobre la que se levanta la propagación sin glitches de la lección 3. Y como todo primitivo reactivo, el memo vive dentro de un dueño: cuando su ámbito se destruye, el nodo se desconecta del grafo y deja de recalcular, sin fugas de memoria ni suscripciones colgando.
En resumen operativo: defines el cálculo una vez, Solid decide cuándo recomputarlo, y tú solo lees. Esa inversión —tú declaras la derivación, el sistema gestiona su vigencia— es la misma que gobierna todo el grafo reactivo y la que hace que Solid sienta menos “manual” que las alternativas basadas en render.
Fija esta consecuencia práctica: llamar a memo() jamás recomputa, solo devuelve el último valor guardado. El cálculo lo dispara el cambio de una fuente, no la lectura. Por eso puedes leer un memo caro cien veces en tu JSX sin remordimiento, y por eso un memo que nadie lee sigue siendo un nodo vivo mientras exista su dueño. Si observas recomputaciones inesperadas, el culpable nunca es un exceso de lecturas: es una fuente que cambia más de lo que creías.
En React, useMemo es una optimización opcional: memoiza para saltarte trabajo entre renders, y si lo quitas todo sigue funcionando, solo más lento. En Solid el memo no va de renders —no hay renders— sino de estructura del grafo. Un createMemo crea un nodo real, permanente, con identidad: observa unas fuentes, cachea un valor y notifica a unos consumidores. Que sea a la vez efecto y signal no es un detalle curioso de implementación: es exactamente lo que lo convierte en el ladrillo con el que se construyen derivaciones compuestas, cortes de propagación y valores caros compartidos. Interioriza esto —observador arriba, observable abajo— y el resto del nivel es consecuencia. En el nuevo núcleo reactivo de Solid 2.0 el memo adopta una evaluación más perezosa de tipo push-pull, pero su contrato observable —cachear y recomputar solo ante cambios reales— se mantiene intacto.
- Crea un signal
ny un memocuadradoque devuelvan() ** 2con unconsole.logdentro. - Lee
cuadrado()cinco veces seguidas en el cuerpo de un componente. ¿Cuántas veces se imprime el log? - Cambia
nconsetNy vuelve a leer. Confirma que el log aparece una sola vez por cada cambio den. - Sustituye el memo por una función normal
const cuadrado = () => n() ** 2y repite: cuenta los logs. Esa diferencia es el cacheo. - Mete un efecto secundario dentro de un memo —un
console.logcon marca de tiempo sirve— y explica por qué desdibuja la frontera entre calcular y actuar.