Saltar al contenido

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.

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

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.

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

TSX
<Tabs>  <Tab label="Perfil">Datos del perfil</Tab>  <Tab label="Ajustes">Panel de ajustes</Tab></Tabs>

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:

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

TSX
// children() para introspeccion: envolver cada opcion en un botonfunction 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().