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