wandres.dev
CHILDREN() · resolver hijos

Composición: compound components e inyección

Composicion avanzada sin cloneElement: compound components como Tabs y Menu mediante context, hijos como funcion para inyectar datos y auto-registro de hijos.

⏱ 17 min

Los compound components<Tabs> con sus <Tab>, <Menu> con sus <Item>— son la prueba de fuego de la composición. En React se resuelven clonando hijos e inyectándoles props. En Solid ese camino está cerrado: no hay descriptores que clonar. La buena noticia es que las alternativas de Solid —context y hijos como función— son más robustas y componen mejor. Este nivel te da el patrón completo.

🎯 Al terminar esta lección sabrás
  • Entender por qué en Solid no existe cloneElement ni su inyección.
  • Construir un Tabs idiomático con context y auto-registro.
  • Inyectar datos con hijos como función (render props).
  • Saber cuándo usar children() para introspección frente a context.

El problema: inyectar sin cloneElement

El patrón React clásico es Children.map + cloneElement(child, { seleccionado }): recorres los hijos-descriptor y devuelves copias con props nuevas. En Solid esto no puede existir, y no por falta de API: un hijo resuelto ya es un nodo DOM con sus props ya aplicadas. No hay plano que reeditar. Así que la comunicación padre-hijo viaja por otras dos vías: el grafo reactivo (context) y el límite de función (render props).

flowchart TD
P[Tabs Provider] --> CTX[TabsContext con api]
CTX --> T1[Tab uno se registra]
CTX --> T2[Tab dos se registra]
T1 --> REG[lista de labels]
T2 --> REG
REG --> BAR[tablist con botones]
P --> BODY[props.children con los paneles]
style CTX fill:#89b4fa,color:#11111b
style REG fill:#a6e3a1,color:#11111b

Patrón A: context (la vía idiomática)

El padre crea un Context con su API reactiva; los hijos la consumen con useContext. Cada Tab se auto-registra al ejecutarse —recuerda: los componentes corren una vez, en orden— y decide si mostrarse según el signal compartido.

import {
  createContext, useContext, createSignal, For, Show, type JSX,
} from "solid-js";

type TabsApi = {
  activo: () => number;
  activar: (i: number) => void;
  registrar: (label: string) => number;
};
const TabsCtx = createContext<TabsApi>();

function Tabs(props: { children: JSX.Element }) {
  const [activo, setActivo] = createSignal(0);
  const [labels, setLabels] = createSignal<string[]>([]);
  const api: TabsApi = {
    activo,
    activar: setActivo,
    registrar(label) {
      const idx = labels().length;
      setLabels((prev) => [...prev, label]);
      return idx;
    },
  };
  return (
    <TabsCtx.Provider value={api}>
      <div role="tablist">
        <For each={labels()}>
          {(label, i) => (
            <button
              role="tab"
              aria-selected={api.activo() === i()}
              onClick={() => api.activar(i())}
            >
              {label}
            </button>
          )}
        </For>
      </div>
      {props.children}
    </TabsCtx.Provider>
  );
}

function Tab(props: { label: string; children: JSX.Element }) {
  const api = useContext(TabsCtx)!;
  const idx = api.registrar(props.label);
  return <Show when={api.activo() === idx}>{props.children}</Show>;
}

El uso queda declarativo y sin fugas de estado:

<Tabs>
  <Tab label="Perfil">Datos del perfil</Tab>
  <Tab label="Ajustes">Panel de ajustes</Tab>
</Tabs>
💡
Registro y limpieza

El auto-registro funciona porque los Tab se ejecutan en orden de documento al montar. Para pestañas dinámicas que aparecen y desaparecen, añade un onCleanup que las des-registre y usa una identidad estable (un id) en lugar del índice, para no desalinear el estado activo cuando se elimina una del medio.

Patrón B: hijos como función

Cuando lo que quieres es inyectar un valor en el hijo, la vía directa es que el hijo sea una función y llamarla con ese valor. Es el equivalente de Solid a las scoped slots de Vue:

function Estado<T>(props: {
  inicial: T;
  children: (par: [() => T, (v: T) => void]) => JSX.Element;
}) {
  const par = createSignal(props.inicial);
  return <>{props.children(par)}</>;
}

// uso: el hijo recibe el estado inyectado
<Estado inicial={0}>
  {([n, setN]) => (
    <button onClick={() => setN(n() + 1)}>Van {n()}</button>
  )}
</Estado>;

Recuerda la regla de aridad del primer nivel: children() no resolvería esta función —tiene un parámetro, así que la deja intacta—. Por eso un componente puede soportar ambas formas y ramificar con typeof props.children === "function".

children() frente a context

Cuándo entra children() en un compound component: cuando el padre necesita introspeccionar el subárbol —contar hijos, envolverlos, leer su orden— en vez de comunicarse con ellos. Context es para hablar con los hijos; children() es para mirarlos. Un SegmentedControl que envuelve cada opción en un botón usa children().toArray(); un Tabs que coordina estado usa context. Muchos componentes maduros combinan ambos: context para el estado, children() para maquetar.

// children() para introspeccion: envolver cada opcion en un boton
function SegmentedControl(props: { children: JSX.Element }) {
  const opciones = children(() => props.children);
  const [activo, setActivo] = createSignal(0);
  return (
    <div class="segmented" role="group">
      <For each={opciones.toArray()}>
        {(op, i) => (
          <button aria-pressed={activo() === i()} onClick={() => setActivo(i())}>
            {op}
          </button>
        )}
      </For>
    </div>
  );
}

Aquí no hay context ni registro: el padre mira sus hijos y los reviste. La regla mnemónica es limpia: si el hijo necesita saber algo del padre, usa context; si el padre necesita hacer algo con la forma de los hijos, usa children().

En Solid, la composición se comunica; no se transforma

Aquí está el cambio de mentalidad que corona el nivel. El reflejo de React es transformar los hijos: los tienes como datos, así que los mapeas, los clonas y les inyectas props antes de renderizarlos. En Solid ese reflejo es un callejón sin salida, porque los hijos no son datos: son efectos y nodos ya materializados. La composición, por tanto, deja de ser una operación de transformación y pasa a ser una de comunicación. El padre no reescribe al hijo; establece un canal —un context reactivo— por el que el hijo lee lo que necesita, cuando lo necesita, y se actualiza de forma quirúrgica sin que nadie lo clone. O bien el padre entrega el control con una función y deja que el hijo decida qué construir con el valor inyectado. Las dos vías comparten filosofía: los hijos son autónomos y colaboran a través de contratos explícitos —el tipo del context, la firma de la función—, no súbditos a los que se les reescriben las props desde arriba. Quien interioriza esto deja de echar de menos cloneElement: descubre que context y render props no son un sucedáneo, sino un modelo de composición más honesto y más componible.

⚔️ Construye un compound component
  1. Implementa Tabs y Tab con context y auto-registro; comprueba que solo se muestra el panel activo.
  2. Añade onCleanup e id estable para soportar pestañas que se añaden y se quitan.
  3. Escribe el componente Estado con hijo como función e inyéctale el par del signal.
  4. Crea un SegmentedControl que use children().toArray() para envolver cada opción y marque la activa por índice.