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.
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á.
- 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/reactivityy 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.
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.
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.
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 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.
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.
- Escribe
Saludoque recibaprops.nombrey lo muestre; pásale un signal desde el padre con un botón que lo cambie. - Cámbialo a
const { nombre } = propsy confirma que deja de actualizarse. Has cortado la arista a propósito. - Prueba la variante
function Saludo({ nombre }): mismo síntoma, otro disfraz. - Repara con acceso directo
props.nombrey verifica que vuelve a fluir. - Instala
eslint-plugin-solid, activasolid/reactivityy comprueba que los pasos 2 y 3 se marcan como error antes siquiera de ejecutar.