Saltar al contenido

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.

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:

TypeScript
// salida conceptual del compiladorcreateComponent(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:

TypeScript
// <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.

El coste oculto de leer dos veces

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

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

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.

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:

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

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.

TSX
const resueltos = children(() => props.children);// MAL: el mismo nodo no puede estar en dos sitios a la vezreturn <><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.