wandres.dev
PROPS · mergeProps, splitProps

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.

⏱ 14 min

Ya sabes que no puedes destructurar props ni fijar defaults con parámetros nativos, porque ambos leen los getters una vez y congelan el valor (7.2). Pero los defaults son una necesidad real: un Boton sin tamano debería asumir md. La respuesta de Solid es mergeProps, un fusionador que combina varios objetos de props en uno solo sin leer nada por adelantado: devuelve un proxy que resuelve cada propiedad en el momento de la lectura, recorriendo sus fuentes hasta encontrar un valor. Es el Object.assign que respeta el grafo.

🎯 Al terminar esta lección sabrás
  • Entender por qué el spread de objetos rompe la reactividad de las props.
  • Dominar la firma de mergeProps y su resolución perezosa en cadena.
  • Dar valores por defecto reactivos que respondan a cambios posteriores del padre.
  • Combinar varias fuentes —tema, contexto, llamada— en un único objeto coherente.

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 congela
function 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ó pisado
📸

Spread de objetos

Lee todos los getters al ejecutarse, copia incluso los undefined, y produce un objeto plano y muerto. Es evaluación ansiosa: decide ahora y pierde el vínculo con la fuente.

🔗

mergeProps

No lee nada al crearse, resuelve en cada acceso, ignora los undefined de más prioridad y devuelve un proxy vivo. Es evaluación perezosa: decide al mirar y conserva las aristas.

mergeProps: 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 define
💡
No es solo para props de componentes

Aunque su nombre apunte a componentes, mergeProps sirve para cualquier objeto reactivo al que quieras dar defaults perezosos: las opciones de un hook, la configuración de un store, los argumentos de una factoría de recursos. Donde tengas “un objeto reactivo más unos valores por defecto que no deben congelarse”, mergeProps es la herramienta correcta.

flowchart LR
R[Lectura de merged punto tamano] --> A{props tiene valor definido}
A -->|si| P[Devuelve el valor del padre]
A -->|no undefined| D[Cae al default md]
P --> Q[Arista viva reevalua]
D --> Q
style R fill:#cba6f7,color:#11111b
style P fill:#a6e3a1,color:#11111b
style D fill:#f9e2af,color:#11111b
style Q fill:#89b4fa,color:#11111b
💡
undefined significa ausente, null no

mergeProps solo deja pasar el default cuando la fuente de más prioridad da undefined. Un null explícito gana al default, porque null es un valor con intención (“no hay imagen”), mientras que undefined es “no me lo pasaron”. Esta distinción, que el spread ignora, es justo la que hace que los defaults se comporten como esperas.

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

📝
El coste es un getter, no una copia

Quien viene de optimizar renders teme que “resolver en cada acceso” sea caro. No lo es: un acceso a mergeProps es recorrer una lista corta de fuentes y devolver la primera definida, sin clonar nada. Y como en Solid no hay renders, no existe un bucle que multiplique ese coste. Cambias una copia por render —que aquí ni siquiera ocurre— por un getter por lectura: el intercambio siempre sale a favor de la reactividad exacta.

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.

Fusionar fuentes, no valores: diferir la decisión al punto de lectura

La diferencia entre Object.assign y mergeProps es la misma que recorre todo Solid: eager contra lazy, valor contra acceso. Object.assign resuelve ahora quién gana cada clave y escribe el resultado en piedra; pierde toda conexión con el origen porque su trabajo es producir un dato. mergeProps hace lo contrario: no decide nada al construirse, solo guarda las fuentes en orden y monta un proxy que, en cada lectura, vuelve a preguntar “de estas capas, ¿cuál tiene hoy algo que decir sobre esta clave?”. La reactividad sobrevive porque nunca se materializa un valor intermedio que la mate. Esta es la lección arquitectónica que se repite en splitProps (7.4) y en los stores: en un sistema de grano fino no combinas estados, combinas maneras de acceder al estado, y dejas que la propagación resuelva lo concreto cuando alguien pregunta. Quien piensa en Solid deja de preguntarse “¿qué valor tiene esto ahora?” y empieza a diseñar “¿por qué camino se resolverá esto cuando se lea?”. mergeProps es ese cambio de mentalidad hecho función.

⚔️ Construye defaults que respiren
  1. Escribe Etiqueta con mergeProps({ color: "gris" }, props) y muéstralo; confirma que sin color sale gris.
  2. Pásale color={c()} desde un signal y cambia c: verifica que el color sigue al signal, no se queda en gris.
  3. Pásale color={undefined} de forma explícita y comprueba que reaparece el default; luego color={null} y observa que gana null.
  4. Reemplaza mergeProps por { color: "gris", ...props } y repite el paso 2: constata que se congela.
  5. Añade una tercera fuente intermedia (un objeto de “tema”) y razona el orden de prioridad de las tres capas.