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.
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.
- Entender por qué en Solid no existe
cloneElementni su inyección. - Construir un
Tabsidiomá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>
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().
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.
- Implementa
TabsyTabcon context y auto-registro; comprueba que solo se muestra el panel activo. - Añade
onCleanupeidestable para soportar pestañas que se añaden y se quitan. - Escribe el componente
Estadocon hijo como función e inyéctale el par del signal. - Crea un
SegmentedControlque usechildren().toArray()para envolver cada opción y marque la activa por índice.