wandres.dev
PROPS · mergeProps, splitProps

children y props especiales: el borde del modelo

children es una prop reactiva más, y resolverla mal duplica nodos del DOM; el helper children la memoiza y la normaliza. A su lado viven las props especiales del compilador —ref, manejadores de eventos, classList, style, spread y el componente Dynamic— y el patrón de componentes controlados con callbacks como fuente única de verdad.

⏱ 16 min

Las props no son todas iguales. Casi todas son datos que fluyen, pero unas pocas tienen significado propio para el compilador y el runtime de Solid: children, ref, los manejadores de eventos, classList, style, el spread. Y children es la más especial de todas, porque no transporta un valor sino DOM potencial: nodos que se crearán cuando alguien los lea. Manejarla sin cuidado duplica elementos o los crea antes de tiempo. Esta lección recorre ese borde del modelo de props, donde la reactividad se encuentra con la plataforma.

🎯 Al terminar esta lección sabrás
  • Entender children como prop reactiva y usar el helper children para resolverla.
  • Reconocer las props especiales del compilador: ref, eventos, classList, style, spread.
  • Componer con Dynamic para elegir el elemento o componente en tiempo de ejecución.
  • Construir componentes controlados con callbacks como única fuente de verdad.

children es una prop, y peligrosa de leer varias veces

Cuando escribes <Panel>contenido</Panel>, ese contenido llega como props.children. Es una prop dinámica como cualquier otra, con un matiz explosivo: leer props.children crea los nodos. Cada acceso puede instanciar de nuevo los elementos, así que leerla dos veces —una para medir, otra para pintar— produce dos copias del DOM. La solución es el helper children, que envuelve la lectura en un memo: resuelve los hijos una sola vez, los memoiza y te da un accesor estable.

import { children } from "solid-js";

function Panel(props) {
  const resueltos = children(() => props.children);

  createEffect(() => {
    const nodos = resueltos(); // array normalizado, creado una sola vez
    console.log("hijos:", Array.isArray(nodos) ? nodos.length : 1);
  });

  return <section class="panel">{resueltos()}</section>;
}

children hace dos favores: memoiza (los nodos se crean una vez, aunque los leas mil) y normaliza (desenvuelve funciones, aplana arrays anidados, te da algo uniforme sobre lo que iterar). Siempre que necesites inspeccionar o manipular los hijos —contarlos, envolver cada uno, leer sus props— pasa por children, nunca por props.children crudo.

Y como resueltos es un memo, deriva de él con naturalidad: envolver cada hijo, filtrarlos o inyectarles props es solo transformar el array que devuelve dentro de otro scope reactivo, de modo que si los hijos cambian, tu transformación se rehace sola. Ese es el mecanismo detrás de componentes como Tabs o Menu, que leen sus hijos para saber cuántos son y qué props declaran, sin que el usuario tenga que numerarlos a mano.

💡
children también puede ser una función

El patrón render-prop existe en Solid: <Lista>{(item) => <Fila dato={item} />}</Lista>. Aquí props.children es una función, no nodos. El helper children la resuelve igual, pero cuando esperas una función explícita, invócala tú: props.children(valor). Es la base de componentes que ceden el control del renderizado a quien los usa.

El flujo del helper, de un vistazo:

flowchart TD
C[props punto children] --> H[helper children memoiza]
H --> R[Resuelve una sola vez]
R --> N[Normaliza funciones y arrays]
N --> A[Accesor estable]
A --> I[Inspeccionar contar o envolver]
A --> P[Pintar en el JSX]
style C fill:#89b4fa,color:#11111b
style H fill:#cba6f7,color:#11111b
style A fill:#a6e3a1,color:#11111b

Las props especiales del compilador

Algunas props no se pasan como datos: el compilador las interpreta. Conviene tenerlas mapeadas, porque su comportamiento no se deduce del modelo general de getters:

🎯

ref

ref={el} te entrega el nodo real del DOM, asignado antes de onMount. Como prop reenviable, props.ref deja que un envoltorio exponga el elemento interno hacia arriba.

Eventos

onClick usa delegación de eventos para rendimiento; on:click adjunta un listener nativo directo, ideal para eventos personalizados; oncapture: engancha la fase de captura.

🎨

classList y style

classList={{ activo: esActivo() }} conmuta clases de forma reactiva y quirúrgica; style={{ color: c() }} acepta un objeto y actualiza solo la propiedad que cambia.

📦

Spread y namespaces

{...otras} reenvía props reactivamente; attr:, prop: y bool: fuerzan atributo, propiedad del elemento o atributo booleano cuando la heurística por defecto no basta.

Estas son el punto donde el grafo reactivo toca la plataforma web. El compilador las cablea a operaciones concretas del DOM —añadir un listener, alternar una clase, fijar una propiedad— manteniendo la reactividad de grano fino: si esActivo() cambia, solo se toca esa clase, no se repinta nada más.

Combinadas, componen la anatomía típica de un componente de entrada: una ref al nodo real, una clase reactiva y un listener directo, todo en el mismo elemento.

function Entrada(props: { invalido: boolean; onTexto: (v: string) => void }) {
  let campo!: HTMLInputElement;                 // ref local al nodo
  return (
    <input
      ref={campo}                               // asignada antes de onMount
      classList={{ error: props.invalido }}     // conmuta la clase de forma quirúrgica
      on:input={(e) => props.onTexto(e.currentTarget.value)} // listener nativo directo
    />
  );
}

Dynamic: elegir el componente en caliente

Cuando el propio elemento o componente a renderizar depende de una prop —un botón que a veces es <a> y a veces <button>, un componente polimórfico con prop as— el compilador no puede saberlo de antemano. Para eso está Dynamic, que recibe qué renderizar como valor y le hace spread del resto:

import { Dynamic } from "solid-js/web";

function Texto(props: { as?: string; children: JSX.Element }) {
  const [local, resto] = splitProps(props, ["as"]);
  return <Dynamic component={local.as ?? "span"} {...resto} />;
}
// <Texto as="h1">Título</Texto>  ->  <h1>Título</h1>

El valor de component puede ser tanto una etiqueta en texto —div, h1— como un componente de verdad, lo que convierte a Dynamic en la pieza para renderizar componentes elegidos en tiempo de ejecución: un mapa de tipo a componente en un CMS, un editor por bloques, un sistema de plugins. Dynamic cierra el círculo del nivel: props reactivas que deciden no solo qué datos, sino qué estructura. Y como local.as es una lectura viva, cambiar la prop en caliente re-monta el elemento correcto sin que tú orquestes nada.

Componentes controlados: el callback como fuente de verdad

El último patrón une props de entrada y props de salida. Un componente controlado no guarda su propio estado: recibe el valor por una prop y notifica los cambios por un callback, dejando que el padre sea la única fuente de verdad. La reactividad de grano fino lo hace trivial y sin el ceremonial de React.

function Campo(props: { valor: string; onCambio: (v: string) => void }) {
  return (
    <input
      value={props.valor}                       // entra por prop reactiva
      onInput={(e) => props.onCambio(e.currentTarget.value)} // sale por callback
    />
  );
}

// El padre posee el estado; el hijo solo lo refleja y lo propone:
const [texto, setTexto] = createSignal("");
<Campo valor={texto()} onCambio={setTexto} />;

props.valor mantiene el <input> sincronizado con el signal del padre, y onCambio sube cada pulsación. No hay estado duplicado, no hay desincronización posible: el dato vive en un único sitio y el componente es una ventana bidireccional sobre él.

📝
Controlado contra no controlado

Un componente no controlado guardaría su propio signal interno y solo avisaría al padre de vez en cuando; es cómodo, pero abre la puerta a dos verdades que se desincronizan. El patrón controlado de arriba renuncia a todo estado local: la prop es la única fuente. La regla práctica: si el padre necesita leer o imponer el valor, hazlo controlado; si el valor es puramente interno y efímero, un signal local basta. En Solid la elección es barata porque, controlado, no pagas ningún re-render por teclear: solo se actualiza el nodo afectado.

Las props especiales son el contrato entre el grafo y la plataforma

Todo el nivel 7 ha defendido una tesis: las props son aristas de un grafo reactivo, no datos. Esta última lección muestra dónde ese grafo se enchufa al mundo real, y por qué el diseño de Solid es tan coherente hasta el borde. children es una prop cuya lectura materializa DOM, así que se rodea de un helper que memoiza esa materialización; los eventos, classList, style y el spread son props que el compilador traduce a mutaciones quirúrgicas del DOM en lugar de a re-renders; Dynamic extiende la reactividad de los datos a la estructura misma; y el patrón controlado convierte una prop de entrada más un callback de salida en un enlace bidireccional sin estado redundante. En ningún punto se rompe el principio de grano fino: cada arista, especial o no, actualiza exactamente el trozo de plataforma del que es responsable, y nada más. Cuando ves children, ref, onInput y Dynamic no como casos aparte sino como el mismo modelo de props tocando distintos rincones del DOM, dejas de memorizar API y empiezas a leer Solid como lo que es: un lenguaje coherente para describir cómo un grafo de valores gobierna una página viva.

⚔️ Del helper al componente controlado
  1. Escribe Envoltura que reciba children, use el helper children y cuente sus hijos en un createEffect.
  2. Renderiza los hijos dos veces en el JSX; primero con props.children (observa la duplicación) y luego con el accesor memoizado (observa que se resuelve una vez).
  3. Crea Boton polimórfico con Dynamic y prop as, y renderízalo como button y como a.
  4. Construye Campo controlado con valor y onCambio; conéctalo a un signal del padre y confirma que dos Campo que comparten el mismo signal se mantienen sincronizados.
  5. Cambia un evento de onClick a on:click y razona cuándo importa la diferencia entre delegación y listener nativo.