Saltar al contenido

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.

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—:

TSX
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 recalculaarea(); // idem: "calculando area" se imprimió una sola vezsetRadio(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:

TSX
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.

TSX
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:

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.

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:

  1. Marcado. El signal notifica a sus observadores; el memo se marca sucio —pendiente de recalcular—, pero todavía no corre.
  2. Recomputo. En la fase de actualización, el cuerpo del memo se ejecuta con las fuentes ya frescas y produce un nuevo valor.
  3. Comparación. El nuevo valor se compara con el anterior mediante equals; volverás a este paso, decisivo, en la lección 4.
  4. Notificación. Solo si el valor cambió, el memo despierta a sus propios observadores y la onda continúa aguas abajo.
TSX
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:

TSX
// 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:

TSX
const informe = createMemo(() => resumir(filas())); // caro, una sola vezconst 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.