wandres.dev
SETUP Y JSX DE SOLID · no es React

Binding: atributos, propiedades y spread

Cómo liga Solid los valores al DOM: la distinción entre atributo HTML y propiedad del nodo, los namespaces attr:, prop: y bool: para forzar una u otra, el spread reactivo de props, las expresiones que se re-ejecutan en atributos, y el tratamiento de los booleanos.

⏱ 13 min

Escribir <input value={texto()} /> parece trivial, pero esconde una decisión que Solid toma por ti en cada binding: ¿esto es un atributo del HTML o una propiedad del nodo del DOM? La distinción no es académica —determina si un valor se refleja, si se actualiza en vivo, si funciona en componentes web—. Solid acierta casi siempre solo, y cuando no, te da namespaces para tomar el mando. Este es el modelo completo de ligadura.

🎯 Al terminar esta lección sabrás
  • Distinguir un atributo HTML de una propiedad del DOM y saber cuál usa Solid por defecto.
  • Forzar la ligadura con los namespaces attr:, prop: y bool:.
  • Aplicar el spread reactivo {...props} y entender por qué sigue siendo reactivo.
  • Manejar expresiones reactivas en atributos y el caso especial de los booleanos.

Atributo no es lo mismo que propiedad

Un elemento del DOM tiene dos superficies que se parecen y no coinciden. Los atributos son lo que escribes en el HTML —cadenas de texto, siempre—; las propiedades son campos del objeto JavaScript que representa al nodo, y pueden ser de cualquier tipo. setAttribute("value", "hola") fija el valor inicial; la propiedad .value refleja el valor actual, que el usuario puede haber cambiado al teclear. No son sinónimos: divergen en cuanto hay interacción.

Solid conoce, para los elementos HTML estándar, qué nombres son propiedades reales y cuáles atributos, y elige la vía correcta automáticamente. Por eso value, checked o textContent se asignan como propiedades, mientras que class, id o data-* se escriben como atributos. En el 95% de los casos no piensas en esto.

// Solid decide: value como propiedad, class como atributo
<input value={texto()} class={clase()} />

Esta distinción es justo la que hace funcionar un input controlado. Al ligar value como propiedad, Solid escribe el campo vivo del nodo —el mismo que el usuario altera al teclear—; si usara el atributo, solo fijaría el valor inicial y el campo se desincronizaría en cuanto alguien escribiese. En la práctica es la diferencia entre un formulario que responde y uno que parece congelado pese a que el estado cambia. El reverso también instruye: class, id o los data-* no tienen una propiedad viva que el usuario modifique, así que se escriben como atributos y ahí permanecen. Solid mantiene internamente la tabla de qué nombres son propiedades reales de cada elemento HTML, y es esa tabla la que consulta para decidir.

El problema aparece en la frontera: componentes web y atributos personalizados, donde Solid no puede saber tu intención. Para eso están los namespaces explícitos.

flowchart TD
A[valor en el JSX] --> B{Solid conoce el destino}
B -->|si| C[elige atributo o propiedad solo]
B -->|caso ambiguo| D[usa un namespace explicito]
D --> E[prop fuerza propiedad del nodo]
D --> F[attr fuerza atributo del html]
D --> G[bool fuerza atributo booleano]

Forzar la vía: attr:, prop: y bool:

Cuando la heurística no basta, los namespaces mandan sobre ella:

🔧

prop:

prop:valor={x} asigna siempre una propiedad del nodo, aunque el nombre no sea estándar. Imprescindible para componentes web que exponen datos ricos por propiedad.

🏷️

attr:

attr:mi-dato={x} escribe siempre un atributo, aunque coincida con una propiedad conocida. Útil para atributos personalizados o ARIA que deben quedar en el HTML.

🔘

bool:

bool:hidden={x} trata el valor como atributo booleano: presente si es verdadero, ausente si es falso. Controla la presencia, no el texto del atributo.

// un componente web que espera datos por propiedad y una marca por atributo
<mi-grafica prop:datos={serie()} attr:data-tema={tema()} />

Estos tres no agotan la lista: on: y oncapture: atan eventos nativos, use: invoca directivas personalizadas y ref captura el nodo. Todos comparten la misma naturaleza —una instrucción explícita de cómo ligar— y todos se resuelven en tiempo de compilación, no en ejecución.

El spread reactivo de props

Solid soporta el spread {...props} en el JSX, y aquí ocurre algo que sorprende a quien viene de React: el spread sigue siendo reactivo. El compilador no copia los valores una vez; genera una ligadura que vigila el objeto y actualiza cada atributo o propiedad cuando su fuente cambia.

function Boton(props) {
  // reparte todas las props sobre el nodo, y cada una queda ligada
  return <button {...props}>Aceptar</button>;
}

Esto es posible porque las props de Solid no son un objeto plano: son un objeto con getters reactivos. Leer props.disabled dentro de un efecto suscribe a ese campo; el spread hace exactamente eso por cada clave. La consecuencia práctica es doble. Primero, nunca destructures las props en la firma —function Boton({ disabled })— porque eso lee los getters una vez y congela su valor, rompiendo la reactividad. Segundo, para combinar props con valores propios sin perder reactividad usas mergeProps, no el spread de objetos de JavaScript.

import { mergeProps } from "solid-js";

function Boton(props) {
  // valores por defecto sin romper la reactividad de las props
  const c = mergeProps({ tipo: "button" }, props);
  return <button type={c.tipo} disabled={c.disabled} />;
}

Su complemento es splitProps, la forma correcta de “descomponer” props sin congelarlas: parte el objeto en grupos que conservan sus getters. Es lo que usas para consumir unas props tú y reenviar el resto con el spread.

import { splitProps } from "solid-js";

function Campo(props) {
  // separa las props propias del resto, sin perder reactividad
  const [propias, resto] = splitProps(props, ["etiqueta"]);
  return (
    <label>
      {propias.etiqueta}
      <input {...resto} />
    </label>
  );
}
⚠️
Destructurar props mata la reactividad

El reflejo de React function Comp({ valor }) es, en Solid, un error silencioso. Al destructurar en la firma lees el getter reactivo en el instante en que el componente se ejecuta —una sola vez— y guardas su valor de ese momento. A partir de ahí, aunque el padre cambie esa prop, tu variable local nunca se entera. No hay excepción ni aviso: simplemente la UI deja de actualizarse. Accede siempre por props.valor, o descompón con splitProps cuando necesites separar grupos de props conservando los getters.

Expresiones reactivas y booleanos

Cualquier expresión que pongas en un atributo y que lea un signal se vuelve reactiva: el compilador la envuelve en un efecto que la reevalúa cuando su dependencia cambia. No pasas la función del signal, la invocas dentro del JSX; el compilador se encarga de que esa lectura quede rastreada.

// cada atributo se reevalua cuando su signal cambia
<a href={`/perfil/${id()}`} title={etiqueta()} tabindex={activo() ? 0 : -1} />

Los atributos booleanosdisabled, checked, readonly, hidden— reciben un trato especial gobernado por la veracidad del valor. Si la expresión es verdadera, Solid pone el atributo o la propiedad; si es falsa, lo quita. No escribes la cadena "true"; pasas un booleano y Solid traduce presencia y ausencia.

// disabled aparece o desaparece segun el booleano
<button disabled={enviando()}>Guardar</button>

// para atributos no estandar, bool: hace lo mismo de forma explicita
<div bool:data-abierto={abierto()} />

Un matiz que revela la granularidad de Solid: cada expresión reactiva de un atributo genera su propia suscripción. En <a href={u()} title={t()} />, cambiar u() no reevalúa title, y al revés. Los objetos classList y style afinan aún más, comparando clave a clave, de modo que activar una clase no toca a las demás. La unidad reactiva no es el elemento: es cada binding por separado.

El binding es la costura entre el grafo reactivo y el DOM

Todo lo que Solid hace se reduce, en el fondo, a una pregunta repetida miles de veces por segundo: cuando este valor reactivo cambie, ¿qué trozo exacto del DOM debo tocar y cómo? El binding es la respuesta a esa pregunta, y por eso merece más atención de la que suele recibir. Cada atributo, cada propiedad, cada spread es una costura individual entre un nodo del grafo reactivo y una superficie concreta del DOM, y Solid la cose de la forma más barata posible: un efecto minúsculo que solo se dispara cuando su dependencia se mueve, y que solo altera ese punto. No hay re-render que reevalúe el elemento entero, no hay diff que decida si el atributo cambió; hay una suscripción directa entre una fuente y un destino. Comprender la distinción atributo/propiedad no es pedantería: es saber qué superficie estás cosiendo. Un value como propiedad refleja lo que el usuario teclea; como atributo, solo fija el valor inicial y luego mientes. Un componente web espera datos ricos por propiedad y se atraganta si le mandas la cadena [object Object] como atributo. Los namespaces prop:, attr: y bool: existen porque, en la frontera de lo estándar, tú sabes la intención y el compilador no. Dominar el binding es dejar de ver <input value={x()} /> como una línea mágica y empezar a verla como lo que es: una declaración precisa de qué signal alimenta qué campo de qué nodo, y con qué semántica.

⚔️ Cose tus propias ligaduras
  1. Crea un <input value={texto()} /> controlado, teclea en él y comprueba con las herramientas de desarrollo cómo la propiedad .value diverge del atributo value inicial.
  2. Escribe un componente Boton(props) que haga {...props} sobre un <button> y confirma que cambiar una prop desde el padre actualiza el botón sin destructurar.
  3. Rompe la reactividad a propósito destructurando props en la firma; observa que la UI se congela, y arréglalo volviendo a props.x.
  4. Liga un atributo booleano disabled a un signal y alterna su valor; luego fuerza un attr:data-estado y un bool:hidden con namespaces y verifica el HTML resultante.