wandres.dev
CHILDREN() · resolver hijos

El helper children(): resolver y memoizar

Qué construye el helper children por dentro: envuelve la lectura de los hijos en un memo, resuelve las expresiones reactivas a nodos concretos y devuelve un getter estable con toArray.

⏱ 15 min

children() es una de esas piezas minúsculas de Solid que, mal entendida, produce bugs desconcertantes —nodos duplicados, estado que se bifurca, hijos que se esfuman— y, bien entendida, desbloquea toda la composición avanzada. El nivel 7 la presentó como el borde del modelo de props; aquí la abrimos por dentro hasta que children(() => props.children) deje de ser una fórmula que copias y pase a ser mecánica que gobiernas. No es azúcar sintáctico: resuelve un problema real del modelo de ejecución de grano fino.

🎯 Al terminar esta lección sabrás
  • Entender qué construye children(() => props.children) por dentro.
  • Ver que devuelve un getter memoizado, no un valor ni un array.
  • Manejar resueltos() y resueltos.toArray().
  • Interiorizar que la resolución es ansiosa y qué implica.

La firma y lo que devuelve

children() recibe una función que devuelve JSX.Element —casi siempre el thunk () => props.children— y devuelve un accesor reactivo: una función que llamas para leer los hijos ya resueltos.

import { children, type JSX } from "solid-js";

function Marco(props: { children: JSX.Element }) {
  const resueltos = children(() => props.children);
  return <section class="marco">{resueltos()}</section>;
}

Fíjate en dos detalles. Primero, resueltos no es el array de hijos: es una función. La invocas —resueltos()— para leer el contenido. Segundo, le pasas un thunk, no props.children directamente. La razón es central: props.children es un getter; si lo leyeras aquí, lo evaluarías una vez, fuera de todo seguimiento reactivo. Al envolverlo en () => props.children, dejas que children() decida cuándo y dentro de qué memo leerlo.

flowchart TD
A[props.children thunk] --> B[createMemo lee los hijos]
B --> C[resolve recorre y ejecuta thunks]
C --> D[getter resueltos memoizado]
D --> E[resueltos devuelve nodos]
D --> F[resueltos.toArray normaliza a lista]
style A fill:#89b4fa,color:#11111b
style D fill:#cba6f7,color:#11111b

Qué hace por dentro

La implementación real cabe en unas líneas. Este es su modelo mental fiel:

import { createMemo } from "solid-js";

function children(fn: () => JSX.Element) {
  const leer = createMemo(fn);                         // 1. lee props.children con tracking
  const resueltos = createMemo(() => resolve(leer())); // 2. resuelve a nodos y memoiza
  (resueltos as any).toArray = () => {
    const c = resueltos();
    return Array.isArray(c) ? c : c != null ? [c] : [];
  };
  return resueltos;
}

function resolve(nodo: unknown): unknown {
  if (typeof nodo === "function" && !(nodo as Function).length)
    return resolve((nodo as Function)());   // thunk de 0 args: se ejecuta
  if (Array.isArray(nodo)) return nodo.flatMap(resolve);
  return nodo;                              // nodo DOM, string, number, null: intacto
}

Son dos memos anidados. El primero aísla la lectura de props.children. El segundo recorre lo leído y lo resuelve: ejecuta las funciones sin argumentos —las expresiones reactivas que el compilador genera para los hijos dinámicos—, aplana los arrays anidados y deja intactos los nodos DOM, los textos y los números.

Que sean memos tiene una consecuencia de ciclo de vida: children() necesita un owner. Como cualquier createMemo, se apoya en el sistema de propiedad de Solid para limpiar sus suscripciones al desmontar el componente. En el nuevo núcleo reactivo de Solid 2.0 cambia parte del motor de propagación, pero este contrato es idéntico: un memo que lee una vez, resuelve y cachea.

📝
Llámalo arriba, una vez

Trata children() como un primitivo disciplinado: invócalo en el nivel superior del cuerpo del componente, nunca dentro de un if, un bucle o un manejador de eventos. Necesita el owner activo para registrar la limpieza de sus memos; fuera de él, ni rastrea ni se limpia bien. Una llamada por punto de uso, siempre en el mismo sitio.

ℹ️
La aridad decide el trato

resolve solo ejecuta funciones cuya length es 0 —thunks sin parámetros—. Una función-hijo que declara un parámetro (una render prop, (x) => ...) tiene length 1 y se deja sin tocar. Esta distinción por aridad es la que permite convivir hijos reactivos y hijos-callback en el mismo mecanismo, y la explotaremos en el nivel de composición.

El getter y toArray

resueltos() devuelve lo que haya: un único nodo si el hijo es uno, o un array si son varios. Por eso el helper adosa toArray(), que siempre entrega una lista —vacía, de uno o de muchos— y te ahorra ramificar según la forma.

function Contar(props: { children: JSX.Element }) {
  const resueltos = children(() => props.children);
  return <p>Hay {resueltos.toArray().length} hijos.</p>;
}

Como ambos memos son reactivos, leer resueltos() dentro de un JSX o de un createEffect rastrea el valor: si los hijos cambian —porque vienen de un <For> o de un <Show>— el memo se recomputa y quien lee se actualiza. Un getter vivo, no una foto.

En la práctica, este par cubre casi todo: resueltos() para insertar, resueltos.toArray() para inspeccionar. El resto del nivel lo explota: primero entenderemos por qué era imprescindible, y después lo usaremos para filtrar, envolver e inyectar. Interioriza la firma antes de seguir: dos memos, un getter, un toArray, resolución ansiosa.

🎯

Una evaluación

props.children se lee una sola vez, dentro del primer memo. No importa cuántas veces llames a resueltos(): la lectura cara ocurre una vez.

🧊

Nodos estables

Los nodos resueltos quedan cacheados. Cada resueltos() devuelve las mismas instancias, no copias nuevas.

🔁

Reactivo

Si los hijos cambian, el memo se recomputa y propaga. El getter sigue el grafo como cualquier otro createMemo.

📚

toArray

Normaliza la forma: nodo suelto o lista, siempre obtienes un array iterable con el que filtrar y envolver.

La resolución es ansiosa

Aquí está el detalle que casi nadie documenta. createMemo ejecuta su cómputo de inmediato, al crearse. Por tanto children(() => props.children) materializa los hijos en el acto, en el cuerpo del componente, aunque nunca los insertes.

function Panel(props: { abierto: boolean; children: JSX.Element }) {
  const resueltos = children(() => props.children); // crea los nodos YA
  return <Show when={props.abierto}>{resueltos()}</Show>;
}

Aunque props.abierto sea false, los hijos ya se construyeron: sus componentes corrieron y sus efectos se montaron. Si buscabas pereza —crearlos solo al abrir— acabas de perderla.

⚠️
children() rompe la pereza de Show

Si los hijos son caros o tienen efectos que no deben correr hasta mostrarse, no los envuelvas en children() por encima del <Show>. Lee props.children directamente dentro del <Show> —donde la evaluación sí es perezosa— o mueve el children() al interior del hijo condicional. Regla: usa children() cuando necesitas inspeccionar o reutilizar los hijos, no como reflejo automático.

children() es el puente hacia un mundo sin descriptores

En React los hijos son descriptores inertes: objetos que describen qué renderizar y que puedes guardar, contar y transformar sin coste. En Solid ese objeto intermedio no existe: un hijo, al evaluarse, ya es un nodo DOM real. Eso hace a Solid rapidísimo, pero abre un hueco: cómo sostienes los hijos como un valor estable para inspeccionarlos o iterarlos sin recrearlos a cada toque. Ese hueco lo llena children(). Al envolver la lectura en un memo, convierte una expresión que fabricaría nodos cada vez que la rozas en un valor materializado una sola vez y cacheado. No es una utilidad de conveniencia: es la pieza que reconcilia la ergonomía de “tratar los hijos como datos” con un motor que no tiene datos, solo efectos y nodos. Comprender esto es entender por qué Solid la necesita y React no: no estás optimizando una lectura, estás dándole a un mundo de puros efectos la ilusión, controlada y segura, de un valor.

⚔️ Desarma el helper
  1. Escribe un Marco que use children(() => props.children) y renderice resueltos().
  2. Añade un <p> que muestre resueltos.toArray().length y comprueba que reacciona si envuelves los hijos en un <For> sobre un signal.
  3. Pon un console.log en el cuerpo de un componente hijo y verifica que corre en el acto al crear el children(), incluso bajo un <Show when={false}>.
  4. Reescribe el caso perezoso leyendo props.children dentro del <Show> y observa cómo el log deja de correr hasta abrir.