Dynamic: el componente elegido en runtime
Renderizar en tiempo de ejecucion una etiqueta nativa o un componente que no conoces al escribir el codigo, con la prop component de Dynamic: registros por tipo, uniones discriminadas, polimorfismo con as y la semantica reactiva del cambio de componente.
No siempre sabes al escribir el código qué vas a renderizar. El tipo de un bloque llega del servidor, el nivel de un encabezado depende de la jerarquía del documento, un sistema de plugins registra componentes que tú nunca importaste. Para todos esos casos existe Dynamic: recibe en component un valor —una cadena con el nombre de una etiqueta o una función componente— y lo materializa en runtime, de forma reactiva. Es la pieza que convierte “qué renderizar” en un dato más del grafo.
- Renderizar una etiqueta nativa o un componente elegidos en tiempo de ejecución con
Dynamic. - Mapear una unión discriminada a componentes mediante un registro tipado.
- Construir un componente polimórfico con una prop
assobreDynamic. - Entender la semántica reactiva: cambiar
componentdesmonta el subárbol previo.
Qué resuelve Dynamic
Sin Dynamic, elegir un componente en runtime degenera en una escalera de Show o en un Switch con un caso por variante: legible con tres opciones, insostenible con quince y directamente imposible cuando el conjunto es abierto —un catálogo de bloques extensible por plugins—. Dynamic colapsa esa escalera en una sola expresión: le das el componente como valor y él lo instancia.
Dynamic vive en solid-js/web y acepta en component tres formas, con esta semántica:
Valor de component |
Qué produce |
|---|---|
una cadena, "section" o "h2" |
crea ese elemento nativo del DOM |
una función Component |
invoca ese componente con el resto de props |
undefined o null |
no renderiza nada, y no lanza error |
El resto de props se reenvían al componente o al elemento resultante. Que un valor falsy no rompa es deliberado: te permite modelar “aún no hay nada que mostrar” sin envolverlo en un Show. Los casos que aparecen una y otra vez son estos:
| Caso | component recibe |
|---|---|
bloques de un CMS según su tipo |
el componente del registro |
| encabezado según la profundidad | una cadena como "h2" |
| variante o vista que se elige sola | un componente guardado en un signal |
| componente polimórfico | la etiqueta o componente de as |
import { Dynamic } from "solid-js/web";
// etiqueta nativa a partir de una cadena calculada en runtime
function Encabezado(props: { nivel: 1 | 2 | 3 | 4; children: JSX.Element }) {
return <Dynamic component={`h${props.nivel}`}>{props.children}</Dynamic>;
}
De la unión discriminada al registro
El caso canónico es un contenido heterogéneo cuyo campo tipo decide su forma. Modelas los datos como una unión discriminada, asocias cada discriminante a un componente en un registro y dejas que Dynamic haga la selección. El registro es un objeto plano; satisfies verifica que cubres todos los casos sin perder los tipos concretos.
import { Dynamic } from "solid-js/web";
import type { Component } from "solid-js";
type Bloque =
| { tipo: "parrafo"; texto: string }
| { tipo: "imagen"; src: string; alt: string }
| { tipo: "cita"; texto: string; autor: string };
const Parrafo: Component<{ texto: string }> = (p) => <p>{p.texto}</p>;
const Imagen: Component<{ src: string; alt: string }> = (p) => (
<img src={p.src} alt={p.alt} />
);
const Cita: Component<{ texto: string; autor: string }> = (p) => (
<figure><blockquote>{p.texto}</blockquote><figcaption>{p.autor}</figcaption></figure>
);
const registro = {
parrafo: Parrafo,
imagen: Imagen,
cita: Cita,
} satisfies Record<Bloque["tipo"], Component<any>>;
function RenderBloque(props: { bloque: Bloque }) {
return <Dynamic component={registro[props.bloque.tipo]} {...props.bloque} />;
}
Recorrer una lista de bloques es entonces trivial, y añadir un tipo nuevo se reduce a escribir su componente y una entrada en el registro —el satisfies te obliga a no olvidarla—:
<For each={bloques()}>{(b) => <RenderBloque bloque={b} />}</For>
El orden del registro no impone el orden de render: la lista de datos manda, y el registro es solo el diccionario que traduce cada tipo a su componente. Esa separación —datos que fluyen por un lado, diccionario estable por otro— es lo que hace el patrón extensible: se añaden variantes al registro sin tocar el componente que las recorre.
Hacer spread de props.bloque sobre Dynamic pasa también el campo tipo como prop; es inofensivo. El punto fino es el tipo del registro: Component<any> es pragmático, pero pierde la correlación entre discriminante y props. Para una correlación estricta —que TypeScript sepa que la variante imagen exige src y alt— necesitas un helper genérico que preserve la unión, a costa de firmas más densas. En librerías de UI reales se acepta el any en la frontera del registro y se confía en la unión de datos aguas arriba.
Polimorfismo: la prop as
La segunda gran aplicación es el componente polimórfico: un mismo componente que renderiza distintas etiquetas según una prop as, conservando el tipado de los atributos de esa etiqueta. Es el patrón que hay detrás de un Box, un Text o un Button de sistemas de diseño como Kobalte.
import { Dynamic } from "solid-js/web";
import { splitProps, type ValidComponent, type ComponentProps } from "solid-js";
function Texto<T extends ValidComponent = "span">(
props: { as?: T } & ComponentProps<T>,
) {
const [propio, resto] = splitProps(props as { as?: T }, ["as"]);
return <Dynamic component={propio.as ?? "span"} {...(resto as ComponentProps<T>)} />;
}
// <Texto as="a" href="/docs">Ir</Texto> -> genera <a href="/docs">Ir</a>
// <Texto>solo texto</Texto> -> genera <span>solo texto</span>
ValidComponent es el tipo que admite Dynamic: una etiqueta conocida o un componente. ComponentProps<T> extrae las props válidas para esa T, de modo que href solo se acepta cuando as vale "a". Los as cast son el peaje de la polimorfia total en TypeScript; el consumidor del componente nunca los ve.
El valor de este patrón es doble: un solo componente cubre <a>, <button>, <div> y cualquier etiqueta futura sin duplicar lógica, y el consumidor conserva el autocompletado y la verificación de tipos propios de la etiqueta elegida. Es la base sobre la que sistemas de diseño como Kobalte construyen componentes accesibles y renombrables sin sacrificar el tipado.
flowchart LR V[valor o tipo en runtime] --> L[lookup en el registro o prop as] L --> D[Dynamic con la prop component] D --> N[nodo real materializado en el DOM] style D fill:#89b4fa,color:#11111b style N fill:#a6e3a1,color:#11111b
Falta la semántica que más sorprende: component es reactivo. Si el valor cambia, Solid desmonta el subárbol anterior y monta el nuevo desde cero; no hay reconciliación entre uno y otro, porque son componentes distintos. No es un re-render con memoria, es una sustitución limpia.
const [vista, setVista] = createSignal<Component>(Tabla);
<Dynamic component={vista()} datos={datos()} />;
// setVista(Grafico): Tabla se desecha por completo y Grafico se crea limpio.
¿Cuándo Dynamic y cuándo Switch? Con un conjunto cerrado y pequeño de ramas heterogéneas —tres estados de una misma vista, con condiciones distintas cada una— Switch es más legible y admite predicados arbitrarios. Con un conjunto abierto o dirigido por datos —un registro extensible, un nombre de etiqueta calculado, un sistema de plugins— Dynamic es la única opción que no crece con cada caso. La regla operativa: si escribirías un case por variante y la variante la decide un dato, sustituye el flujo de control por una tabla y un Dynamic.
Que component acepte undefined sin romper te ahorra un Show de guarda: <Dynamic component={registro[clave()]} /> no pinta nada cuando la clave no existe en el registro, en lugar de lanzar. Es el modo idiomático de modelar “aún no hay componente para esto” y de tolerar datos parciales durante una carga sin ramas extra.
El salto mental de este nivel es dejar de pensar en el componente como algo fijado en el texto del JSX y empezar a tratarlo como un dato de primera clase que fluye por el grafo igual que un número o una cadena. Dynamic es, literalmente, la aplicación de esa idea: component es una prop reactiva, y como toda prop reactiva, cuando cambia, propaga. La consecuencia es doble. Por un lado, la selección de vista deja de ser flujo de control —escaleras de Show, Switch interminables— y pasa a ser una tabla: un mapa de discriminante a componente que se lee en tiempo constante y se extiende sin tocar la lógica de render. Por otro, ganas la polimorfia de etiqueta que en JSX estático es imposible, porque el nombre del elemento estaba clavado en la sintaxis. El precio a interiorizar es que cambiar de componente no preserva estado: cuando entiendes que Dynamic no “cambia” un componente sino que desecha uno y crea otro, dejas de buscar por qué el estado interno se reinició y empiezas a diseñar dónde debe vivir el estado para sobrevivir al cambio —fuera del componente intercambiable, en el owner que lo contiene—.
- Escribe la unión
Bloquecon tres variantes y su registro consatisfies; renderiza una lista mixta conForyRenderBloque. - Añade una cuarta variante y comprueba que TypeScript te obliga a registrarla antes de compilar.
- Implementa
Encabezadoconcomponentcalculado como cadena y verifica que el nivel cambia la etiqueta real en el DOM. - Construye el
Textopolimórfico y usaas="a"conhref; comprueba que sinas="a"el atributohrefdeja de tipar. - Guarda un componente en un signal y cámbialo con un botón; añade un
createSignalinterno al componente y observa que su valor se pierde en el cambio. Muévelo al padre para que sobreviva.