wandres.dev
CHILDREN() · resolver hijos

Por qué props.children re-crea nodos

Por qué leer props.children varias veces vuelve a ejecutar el JSX y crea instancias duplicadas de los componentes hijo, y cómo el memo de children lo evita.

⏱ 15 min

El error más caro con los hijos en Solid no es sintáctico: es conceptual. Quien viene de React asume que props.children es un valor barato que puede leer las veces que quiera. En Solid, cada lectura puede volver a ejecutar el JSX y fabricar nodos —o peor, instancias de componente— completamente nuevos. Este nivel disecciona ese comportamiento y muestra por qué children() es la cura, no un adorno.

🎯 Al terminar esta lección sabrás
  • Ver que props.children es un getter, no un valor cacheado.
  • Entender por qué dos lecturas producen dos instancias de un mismo hijo.
  • Reconocer los síntomas: estado bifurcado, efectos dobles, nodos perdidos.
  • Explicar con precisión cómo children() lo resuelve.

props.children es un getter, no un valor

Cuando escribes <Doble><Contador /></Doble>, el compilador de Solid no guarda el hijo en una variable. Lo compila como un getter en el objeto de props:

// salida conceptual del compilador
createComponent(Doble, {
  get children() {
    return createComponent(Contador, {});
  },
});

Cada vez que alguien lee props.children, se ejecuta ese getter, que llama a createComponent(Contador, {}) otra vez. No hay caché por defecto. props.children no es “el hijo”: es “la receta para fabricar el hijo”, y se cocina en cada acceso.

Con varios hijos, el getter devuelve un array, y cada elemento dinámico es a su vez perezoso:

// <Doble><A /><B /></Doble> compila, en esencia, a:
createComponent(Doble, {
  get children() {
    return [createComponent(A, {}), createComponent(B, {})];
  },
});

Leer props.children reconstruye el array y vuelve a instanciar A y B. La lección no cambia con la cantidad: mientras no memoices, cada acceso reejecuta el subárbol entero de hijos.

📝
En React esto no existe

En React, props.children es el mismo objeto durante todo el render: leerlo diez veces devuelve diez referencias al mismo descriptor inerte, sin coste ni efectos. Por eso ningún reactista desarrolla el reflejo de “no leer los hijos dos veces”: en su mundo es gratis. Al llegar a Solid, ese reflejo ausente es justo la trampa. La regla nueva —una lectura, o memoiza— no traduce ningún hábito previo: hay que instalarla de cero.

El coste oculto de leer dos veces

Mira este antipatrón, tentador y sutil —comprobar si hay hijos y luego renderizarlos:

function Lista(props: { children: JSX.Element }) {
  const hayHijos = () => props.children != null;   // lectura 1
  return (
    <Show when={hayHijos()} fallback={<p>Sin hijos</p>}>
      <ul>{props.children}</ul>                     {/* lectura 2 */}
    </Show>
  );
}

Cada props.children dispara el getter. Si el hijo es <Contador />, la comprobación crea la instancia A —con su propio signal— y el <ul> crea la instancia B, con estado independiente. La que ves en pantalla no es la que se comprobó. Si el hijo tenía un createEffect, corre dos veces; si abría una conexión, la abre dos veces.

flowchart TD
R1[primera lectura de props.children] --> C1[createComponent Contador]
C1 --> A[instancia A con su propio estado]
R2[segunda lectura de props.children] --> C2[createComponent Contador de nuevo]
C2 --> B[instancia B con estado distinto]
A --> D[dos contadores que divergen]
B --> D
style A fill:#f38ba8,color:#11111b
style B fill:#f38ba8,color:#11111b
style D fill:#fab387,color:#11111b

Los síntomas desconciertan justo porque el código “parece” correcto: un contador que no cuenta donde debe, un onMount que dispara dos veces, un formulario cuyo valor no coincide con lo que ves. La causa raíz siempre es la misma: leíste el getter más de una vez.

Conviene matizar: muchos consumidores de hijos ya memoizan por dentro. <For>, <Show>, <Switch> y el propio <>...</> del JSX leen props.children de forma controlada, así que insertarlos una vez por esas vías es seguro. El peligro nace cuando lees props.children en código imperativo —para medir, contar o decidir— y además lo insertas: son dos lecturas, y ahí aparecen los duplicados.

ℹ️
No siempre necesitas children()

Si solo insertas los hijos una vez —return <div>{props.children}</div>— no hace falta el helper: hay una única lectura y ningún duplicado posible. children() gana cuando el mismo componente lee e inserta, o lee dos veces. Envolver por costumbre incluso resta, porque fuerza la creación ansiosa que viste en 8.1.

Cómo children() lo cura

children() interpone un createMemo sobre esa lectura. El memo ejecuta el getter una sola vez, cachea el resultado —la instancia ya creada— y sirve esa misma referencia en cada acceso posterior:

function Lista(props: { children: JSX.Element }) {
  const resueltos = children(() => props.children); // una evaluacion, cacheada
  return (
    <Show when={resueltos.toArray().length > 0} fallback={<p>Sin hijos</p>}>
      <ul>{resueltos()}</ul>
    </Show>
  );
}

Ahora resueltos() y resueltos.toArray() leen del mismo memo. Hay una única instancia de Contador, un único efecto, un único estado. Cuentas y renderizas exactamente el mismo hijo. El memo es también un límite de reactividad: si los hijos cambian, se recomputa una vez y todos los lectores ven el nuevo valor coherente.

Observa el segundo beneficio, más silencioso: el memo es una frontera de coherencia. Sin él, tus dos lecturas podrían ver estados distintos del mismo hijo en el mismo instante; con él, todos los lectores comparten una foto consistente que solo cambia cuando el memo se recomputa. Coherencia y unicidad son la misma propiedad vista desde dos ángulos.

💡
La regla operativa

Si vas a tocar props.children más de una vez en un mismo componente —para contar, inspeccionar, envolver y además renderizar—, envuélvelo en children(). Si lo insertas una sola vez y no lo inspeccionas, props.children directo es correcto y más perezoso. El helper paga cuando hay lecturas múltiples.

Lo que children() NO hace

Un malentendido habitual: creer que children() te deja duplicar un hijo en dos lugares del DOM. No. Un nodo DOM vive en un solo padre; si insertas la misma referencia en dos sitios, el navegador lo mueve al último, dejando el primero vacío.

const resueltos = children(() => props.children);
// MAL: el mismo nodo no puede estar en dos sitios a la vez
return <><div>{resueltos()}</div><aside>{resueltos()}</aside></>;

children() garantiza una instancia estable, precisamente lo contrario de duplicar. Si de verdad necesitas el contenido en dos posiciones, necesitas dos evaluaciones distintas (dos usos de props.children sin memoizar, asumiendo el coste) o un rediseño con <Portal> o con datos en vez de nodos.

Para renderizar el mismo contenido lógico en dos sitios de verdad —un tooltip y su ancla, un modal y su disparador— la respuesta no es duplicar nodos sino duplicar la descripción: pasa los hijos como una función () => <Contenido /> y llámala en cada punto, o usa <Portal> para teletransportar un único árbol. El helper resuelve el problema de una materialización estable; multiplicar materializaciones es un problema distinto, que se diseña aparte.

El getter es la unidad de trabajo, no el nodo

La intuición que hay que reprogramar es esta: en Solid, props.children no nombra un objeto, nombra un trabajo a realizar. Leerlo es ejecutarlo. React te acostumbró a que los hijos fueran datos inertes que puedes mirar cuantas veces quieras porque el trabajo real —crear DOM— lo hace el reconciliador más tarde y una sola vez. Solid invierte eso: no hay reconciliador ni fase de commit; la creación del DOM es la evaluación del JSX, aquí y ahora. Por eso una segunda lectura no es “mirar otra vez”, es “hacerlo otra vez”, con toda su carga de instancias, efectos y side effects. children() no es una optimización cosmética: es la forma de decir “haz este trabajo una vez y dame el resultado”, que es justo lo que en React obtienes gratis y en Solid tienes que pedir explícitamente. Interiorizar que el getter es la unidad de trabajo —y no el nodo— es lo que separa a quien copia el patrón de quien lo entiende.

⚔️ Provoca y cura el duplicado
  1. Crea un Contador con un signal interno y un createEffect que loguee “montado”.
  2. Escríbelo dentro de un Lista que lea props.children dos veces (para contar y para renderizar) y observa el log doble y el estado bifurcado.
  3. Envuelve con children(() => props.children) y confirma que el log vuelve a ser único.
  4. Intenta insertar resueltos() en dos contenedores y comprueba que el nodo se mueve al segundo. Explica por qué.
  5. Explica por qué en React el mismo antipatrón —leer props.children dos veces— no produce duplicados, y qué propiedad del modelo de Solid lo cambia todo.