mergeProps: valores por defecto y fusión reactiva
mergeProps fusiona varias fuentes de props en un único proxy de getters que resuelve en cadena y conserva la reactividad. Es la forma correcta de dar valores por defecto y de combinar props de tema, contexto y llamada sin congelar nada, justo allí donde el spread de objetos plano falla en silencio.
El spread plano lee y congela
La tentación obvia para poner defaults es el spread de objetos: { ...defaults, ...props }. Parece equivalente, pero es la misma trampa de 7.2 con otro traje. El spread enumera y lee cada propiedad de sus fuentes en el instante en que se ejecuta, dispara todos los getters, y vuelca los resultados en un objeto plano y muerto.
// ROTO: el spread lee todos los getters una vez y los congelafunction Boton(props) { const conDefaults = { tamano: "md", ...props }; // foto, no ventana return <button>{conDefaults.tamano}</button>; // no reacciona}Hay un segundo defecto, más sutil: el spread copia la propiedad aunque su valor sea undefined. Si el padre pasa tamano={undefined}, el objeto resultante tendrá tamano en undefined y pisará tu default, dejándote sin valor. Y es un caso frecuentísimo, porque una prop opcional que no se pasa llega precisamente como undefined. Necesitas algo que resuelva perezosamente, que mantenga vivas las aristas, y que además trate undefined como “ausente” para que el default cumpla su función.
// El spread ni siquiera respeta el default ante undefined:const conFoto = { tamano: "md", ...{ tamano: undefined } };conFoto.tamano; // undefined, NO "md": el default quedó pisadomergeProps: resolución perezosa en cadena
mergeProps(...fuentes) recibe cualquier número de objetos y devuelve un proxy. No lee nada al crearse. Cuando accedes a una propiedad, el proxy recorre las fuentes de derecha a izquierda y devuelve el primer valor definido que encuentra; si todas dan undefined, cae al default de la izquierda. Cada acceso reevalúa, así que los getters de las fuentes reactivas siguen vivos.
import { mergeProps } from "solid-js";function Boton(props: { etiqueta: string; tamano?: string }) { const merged = mergeProps({ tamano: "md" }, props); // default a la izquierda ─┘ └─ prioridad a la derecha return <button class={`btn-${merged.tamano}`}>{merged.etiqueta}</button>;}Aquí merged.tamano da md mientras el padre no pase nada; en cuanto el padre pase un signal como tamano={size()}, la arista queda viva y merged.tamano seguirá sus cambios. El default no es un valor grabado: es el eslabón final de una cadena que solo se consulta cuando los anteriores callan.
Las fuentes pueden ser tan reactivas como quieras. Un store —que es un objeto reactivo— encaja como fuente sin ceremonia, y mergeProps proxifica sobre él conservando su granularidad de acceso:
import { createStore } from "solid-js/store";const [estado, setEstado] = createStore({ tamano: "sm" });const p = mergeProps({ tamano: "md", color: "gris" }, estado);// p.tamano sigue a setEstado; color cae al default porque estado no lo defineVarias fuentes: tema, contexto y llamada
La firma variádica brilla cuando compones capas de configuración. Un componente de diseño suele querer, en orden de prioridad creciente: sus defaults internos, lo que dicte un tema por contexto, y por encima de todo lo que pase quien lo invoca.
import { mergeProps, useContext } from "solid-js";function Card(props: CardProps) { const tema = useContext(TemaContext); // objeto reactivo del proveedor const p = mergeProps( { padding: "1rem", radio: "8px" }, // 1. defaults del componente tema.card, // 2. sobrescribe el tema props, // 3. gana siempre la llamada ); return <div style={{ padding: p.padding, "border-radius": p.radio }}>{p.children}</div>;}Las tres capas conviven en un único objeto de acceso uniforme, y todas conservan su reactividad: si el tema cambia en caliente, o el padre altera una prop, la lectura correspondiente lo refleja. Ese es el poder real de mergeProps: no fusiona valores, fusiona fuentes, y difiere la decisión de cuál gana hasta el instante de mirar. Y el orden se lee como se piensa: de menos a más prioridad, de izquierda a derecha, sin la asimetría confusa del spread donde lo primero es lo que menos manda.
Lo que mergeProps no hace: la fusión es superficial
Un malentendido caro: mergeProps fusiona por clave de primer nivel, no en profundidad. Si dos fuentes traen un objeto anidado bajo la misma clave, la de más prioridad reemplaza el objeto entero; no combina sus campos internos.
const p = mergeProps( { estilo: { color: "gris", peso: "normal" } }, { estilo: { color: "rojo" } }, // reemplaza TODO estilo, peso se pierde);p.estilo; // { color: "rojo" } — no { color: "rojo", peso: "normal" }Si necesitas mezclar el interior de esos objetos, hazlo tú de forma explícita —con otro mergeProps anidado o derivando un memo que los combine— pero sabiendo que sigues obligado a preservar la reactividad de las fuentes internas. La regla mental: mergeProps decide qué fuente responde a cada clave, no cómo se funden los valores de esa clave. Confundir “fusión de fuentes” con “fusión profunda de datos” es el tropiezo clásico al escalar un sistema de diseño.
En la práctica, esa superficialidad es una virtud, no una carencia: mantiene mergeProps predecible y barato. Cuando necesites profundidad, constrúyela tú, explícita y localizada, en lugar de esperar que la fusión adivine cómo combinar dos objetos que quizá ni siquiera comparten forma.