Saltar al contenido

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

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

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

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

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