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.
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.
- Ver que
props.childrenes 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, 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 tú 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.
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.
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.
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.
- Crea un
Contadorcon un signal interno y uncreateEffectque loguee “montado”. - Escríbelo dentro de un
Listaque leaprops.childrendos veces (para contar y para renderizar) y observa el log doble y el estado bifurcado. - Envuelve con
children(() => props.children)y confirma que el log vuelve a ser único. - Intenta insertar
resueltos()en dos contenedores y comprueba que el nodo se mueve al segundo. Explica por qué. - Explica por qué en React el mismo antipatrón —leer
props.childrendos veces— no produce duplicados, y qué propiedad del modelo de Solid lo cambia todo.