wandres.dev
PROPS · mergeProps, splitProps

Por qué NO se destructuran las props

Destructurar las props en Solid rompe la reactividad de la forma más silenciosa posible: leer los getters una sola vez congela sus valores y corta las aristas del grafo. Aquí ves por qué la destructuración es evaluación ansiosa, cómo lo detecta el linter, y cuáles son las alternativas idiomáticas que preservan el flujo.

⏱ 15 min

Es el error que todo el mundo comete al llegar a Solid desde React, porque allí es no solo inofensivo sino idiomático: function Boton({ label, onClick }) { ... }. En Solid esa misma línea es una trampa. Como viste en 7.1, cada prop es un getter vivo; destructurar significa invocar ese getter una sola vez y guardar lo que devuelva en una variable normal. En ese instante cortas la arista con el padre y te quedas con un cadáver: el valor que la prop tenía al montar, que ya nunca cambiará.

🎯 Al terminar esta lección sabrás
  • Ver por qué la destructuración es evaluación ansiosa que congela el valor.
  • Reconocer los tres disfraces del mismo error: rest, parámetros y defaults.
  • Aprender las alternativas idiomáticas que conservan la reactividad.
  • Saber leer el aviso del linter solid/reactivity y por qué existe.

Destructurar es evaluar ya, y una sola vez

La asignación por destructuración no es azúcar sintáctico inocente: es una lectura inmediata. Cuando escribes const { valor } = props, JavaScript accede a props.valor en ese preciso momento, ejecuta el getter, obtiene un número y lo copia en una constante llamada valor. A partir de ahí, valor es un primitivo desconectado: no sabe de dónde vino ni tiene forma de enterarse de que la fuente cambió. Es el mismo error conceptual que llamar a un signal y guardar su número —const n = count()— en lugar de guardar el signal: te quedas con la fotografía y tiras la cámara.

// ROTO: la reactividad muere en la primera línea
function Boton(props: { etiqueta: string }) {
  const { etiqueta } = props;        // getter leído una vez -> valor muerto
  return <button>{etiqueta}</button>; // nunca se actualizará
}

// CORRECTO: cada lectura reevalúa el getter dentro del JSX
function Boton(props: { etiqueta: string }) {
  return <button>{props.etiqueta}</button>; // arista viva
}

La regla se resume en una imagen: accede tarde, no pronto. Mantén la palabra props. delante hasta el último momento posible, dentro de un scope de tracking, para que cada lectura sea una llamada fresca al getter y no una copia rancia. Fíjate en el detalle que engaña: los dos Boton de arriba montan idénticos —el primer valor de etiqueta es correcto en ambos—, así que el bug no se ve al abrir la página. Solo aparece cuando el padre cambia la prop y el roto no reacciona, a veces días después, lejos de la línea culpable.

flowchart TD
P[Signal del padre] --> G[Getter props punto valor]
G -->|acceso directo repetido| V[Lectura viva reactiva]
G -->|destructuracion una vez| M[Valor copiado y congelado]
M --> X[Arista cortada no reacciona]
style P fill:#89b4fa,color:#11111b
style V fill:#a6e3a1,color:#11111b
style X fill:#f38ba8,color:#11111b

Los tres disfraces del mismo error

El fallo se cuela con muchas caras, y todas comparten la raíz de la lectura ansiosa. Conviene reconocerlas al vuelo:

// 1. Destructuración explícita
const { valor } = props;

// 2. Parámetro destructurado (el reflejo de React)
function Boton({ etiqueta, onClick }) { /* ... */ }

// 3. Rest, que además fuerza a leer TODAS las props de golpe
const { clase, ...resto } = props;

El tercero es el más insidioso porque, para construir resto, el operador rest enumera y lee cada propiedad enumerable del objeto en ese instante: dispara todos los getters a la vez y congela el objeto entero. Cualquiera de las tres formas produce lo mismo: valores fotografiados que no volverán a cambiar. Y como el componente monta bien —los valores iniciales son correctos—, el bug es invisible hasta que algo debería moverse y no lo hace.

🧊

Destructuración directa

const { valor } = props lee el getter una vez y guarda un primitivo. La copia ignora de dónde vino: la arista queda cortada en esa misma línea.

🎭

Parámetro destructurado

function C({ valor }) es el reflejo de React. Idéntico daño, disfrazado de firma elegante: el getter se lee al invocar el componente, una sola vez.

💥

Rest

const { ...resto } = props es el peor: para armar resto enumera y dispara todos los getters a la vez, congelando el objeto entero de golpe.

📝
Con objetos el engaño es aún mayor

Podrías creer que esto solo afecta a primitivos y que un objeto, al copiarse por referencia, “seguiría vivo”. No: si el padre reemplaza el objeto entero por otro —lo normal con estado inmutable—, tu referencia destructurada sigue apuntando al viejo. La reactividad de Solid es sobre qué objeto entrega el getter, no sobre mutaciones internas; separar el getter te desconecta también de los reemplazos. Ningún tipo se salva de la regla.

⚠️
Los defaults nativos también congelan

La tentación de function Boton({ tamano = 'md' }) { ... } es doble trampa: destructura y fija el default en la primera lectura. Si tamano llega undefined al montar, se queda en 'md' para siempre, aunque el padre lo defina después. Los valores por defecto reactivos no se hacen así; se hacen con mergeProps, que verás en 7.3.

Las alternativas que sí preservan el flujo

No destructurar no significa repetir props. de forma cansina por todas partes. Solid ofrece herramientas para cada necesidad, todas construidas para mantener las aristas intactas:

import { mergeProps, splitProps } from "solid-js";

function Boton(props: { etiqueta: string; tamano?: string; onClick?: () => void }) {
  // Defaults reactivos sin congelar: 7.3
  const conDefaults = mergeProps({ tamano: "md" }, props);

  // Separar lo propio de lo que reenvías, sin rest destructivo: 7.4
  const [propio, resto] = splitProps(conDefaults, ["etiqueta"]);

  // Derivar sin perder reactividad: envuelve en función o memo
  const clase = () => `btn btn-${conDefaults.tamano}`;

  return (
    <button class={clase()} {...resto}>
      {propio.etiqueta}
    </button>
  );
}

La idea de fondo: cuando necesites un valor por defecto, usa mergeProps; cuando necesites apartar unas props y reenviar el resto, usa splitProps; cuando necesites transformar, envuelve el cálculo en una función o un memo para que se reevalúe. Lo que nunca haces es sacar el valor del getter y guardarlo suelto.

Hay una comodidad legítima que sí se permite: crear un alias en forma de función. Si repetir props.tamano cansa, define const tamano = () => props.tamano y llama a tamano() donde lo necesites. Es una variable, sí, pero contiene el modo de leer, no el valor leído, así que la arista sobrevive. La diferencia es literal y milimétrica: const tamano = props.tamano congela, const tamano = () => props.tamano fluye.

Dónde sí puedes leer directo

La regla no dice “nunca leas una prop en una variable”, dice “no la separes de su getter en un sitio que se ejecuta una sola vez”. Hay un lugar donde acceder directo es no solo seguro sino idiomático: dentro de un manejador de eventos o de cualquier callback diferido. Ahí la lectura ocurre cuando el usuario actúa, no al montar, así que el getter devuelve el valor vigente en ese preciso instante.

function Guardar(props: { texto: string; onGuardar: (t: string) => void }) {
  // props.texto se lee al hacer clic, no antes: el valor siempre es el actual
  return <button onClick={() => props.onGuardar(props.texto)}>Guardar</button>;
}

Lo mismo vale dentro de un createEffect, un createMemo o el JSX: son scopes que se reevalúan, así que la lectura se repite y la arista vive. El pecado no es leer props.x, es guardar el resultado de esa lectura en un sitio que solo corre una vez. Accede tarde y accede a menudo; nunca fotografíes.

📝
La regla en una sola pregunta

Ante cualquier línea dudosa, pregúntate solo esto: ¿esta variable guarda un valor o guarda un modo de leerlo? Un valor reactivo copiado a una constante es una arista cortada; una función que lee la prop es una arista viva. mergeProps, splitProps, los alias en función y el acceso tardío no son cuatro reglas distintas, son cuatro formas de responder “un modo de leerlo” a esa única pregunta.

ℹ️
El linter te cubre las espaldas

El plugin eslint-plugin-solid incluye la regla solid/reactivity, que marca precisamente estos casos: destructurar props, guardar una lectura reactiva en una variable fuera de tracking, o pasar un valor donde se esperaba una función reactiva. No es pedante: cada aviso señala una arista que estás a punto de cortar. Trátalo como parte del compilador, no como ruido.

La reactividad se transporta en la referencia, no en el valor

El principio que subyace a toda esta lección es más grande que las props: en un sistema de grano fino, lo reactivo es el medio de acceso, no el dato. Un signal reacciona porque lo lees llamándolo, count(); una prop reacciona porque la lees a través de su objeto, props.valor. En ambos casos la reactividad viaja en la forma de acceder, no en el número que sale. Destructurar rompe esto porque hace exactamente lo contrario de lo que el sistema necesita: extrae el valor y tira la referencia, se queda con el pez y suelta la caña. Por eso no hay parche posible una vez destructurado —no existe un truco para “re-suscribir” un primitivo suelto—; la única cura es no separarlo nunca de su fuente. Cuando internalizas que en Solid nunca posees un valor sino un modo de pedirlo, dejas de pelearte con la regla del linter y empiezas a escribir componentes que fluyen sin que tengas que pensarlo. Esa es la diferencia entre memorizar “no destructures” y entender por qué destructurar es, literalmente, desconectar el cable.

⚔️ Rompe y repara la reactividad
  1. Escribe Saludo que reciba props.nombre y lo muestre; pásale un signal desde el padre con un botón que lo cambie.
  2. Cámbialo a const { nombre } = props y confirma que deja de actualizarse. Has cortado la arista a propósito.
  3. Prueba la variante function Saludo({ nombre }): mismo síntoma, otro disfraz.
  4. Repara con acceso directo props.nombre y verifica que vuelve a fluir.
  5. Instala eslint-plugin-solid, activa solid/reactivity y comprueba que los pasos 2 y 3 se marcan como error antes siquiera de ejecutar.