wandres.dev
PROPS · mergeProps, splitProps

Las props son un proxy reactivo de getters

En Solid las props no son una foto inmutable como en React: son un objeto vivo de getters, aristas del grafo reactivo disfrazadas de campos. Leerlas dentro de un scope de tracking crea una suscripción quirúrgica y su valor cambia con el tiempo, porque el componente corre una vez pero el grafo sigue latiendo.

⏱ 14 min

Quien llega desde React arrastra un reflejo: las props son los argumentos de una función que se vuelve a llamar en cada render, una foto fija del instante. En Solid ese modelo es falso, y confiar en él es la fuente número uno de bugs reactivos. El componente se ejecuta una sola vez, así que si las props fueran una foto, jamás se actualizarían. La verdad es más sutil y más potente: props es un objeto vivo cuyas propiedades son getters, aristas del grafo reactivo disfrazadas de campos. Leer una prop no consulta un dato: dispara una función.

🎯 Al terminar esta lección sabrás
  • Entender que props es un objeto de getters vivos, no una estructura inmutable.
  • Ver por qué el componente corre una vez pero sus props cambian con el tiempo.
  • Comprender que leer props.x en un scope rastreado crea una suscripción fina.
  • Distinguir la lectura rastreada (JSX, efecto, memo) de la lectura suelta del cuerpo.

El componente corre una vez; las props, no

En Solid el cuerpo de un componente no es una función de render: es un constructor que se ejecuta al montar y nunca más. Todo lo que deba cambiar después vive en el grafo reactivo, no en re-ejecuciones del cuerpo. Aquí aparece la tensión que hay que resolver: si el componente corre una vez y recibiera sus props como valores planos, esos valores quedarían fosilizados en el primer instante y nada volvería a moverse.

El compilador de Solid deshace el nudo transformando cada binding dinámico del JSX en un getter. Cuando escribes esto:

function App() {
  const [n, setN] = createSignal(0);
  return <Visor valor={n()} etiqueta="conteo" />;
}

El compilador no evalúa n() una vez y lo guarda. Genera, en esencia, este objeto de props:

createComponent(Visor, {
  etiqueta: "conteo",   // estática: valor plano
  get valor() {         // dinámica: getter que reevalúa
    return n();
  },
});

La prop estática queda como campo normal; la dinámica se convierte en un getter que, cada vez que lo invocas, vuelve a ejecutar n(). Por eso valor no está fijado a nada: es el resultado actual de una expresión, calculado en el momento exacto de la lectura.

Puedes comprobar el “corre una vez” con un experimento de una línea: un console.log en el cuerpo del componente se imprime una sola vez, por muchas veces que sus props cambien después. El cuerpo es el constructor; las props son el cable que sigue conduciendo cuando el constructor ya terminó.

function Visor(props) {
  console.log("cuerpo de Visor"); // se imprime UNA vez en toda la vida del componente
  return <p>{props.valor}</p>;     // esto, en cambio, se actualiza en cada cambio de valor
}

Leer una prop es llamar a un getter

La consecuencia es directa: props.valor no es un acceso a memoria, es una llamada a función. Y como esa función lee un signal, si la invocas dentro de un scope de tracking —el JSX, un createEffect, un createMemo— Solid registra una suscripción quirúrgica entre ese consumidor y el signal de origen del padre.

function Visor(props: { valor: number; etiqueta: string }) {
  createEffect(() => {
    // se re-ejecuta cada vez que props.valor cambia, y solo entonces
    console.log(props.etiqueta, props.valor);
  });
  return <p>{props.valor}</p>;
}

No hay lista de dependencias, no hay React.memo, no hay comparación de props. El efecto se suscribe a props.valor por el mero hecho de leerlo, y solo a lo que lee: si cambiara una prop que el efecto no toca, no se re-ejecutaría. El tracking es automático y exacto, y cruza la frontera padre-hijo sin que ninguno de los dos lo declare.

Y como leer una prop es solo invocar un getter, puedes reenviarla a un hijo sin desenvolverla, y la arista se prolonga un nivel más abajo, intacta:

function Marco(props: { titulo: string }) {
  // No leemos props.titulo aquí; se lo cedemos al hijo como getter vivo
  return <Cabecera texto={props.titulo} />;
}

El valor de titulo nunca se materializa dentro de Marco: viaja como conexión, no como dato, hasta el punto donde de verdad se lee, ya en el JSX de Cabecera. Esta transparencia es la que permite que cadenas largas de componentes reenvíen estado sin coste y sin perder una sola actualización por el camino.

📸

React: foto por render

Las props son argumentos de una función que se re-invoca. Dentro de un render son constantes; para “ver” un cambio, el componente entero vuelve a ejecutarse.

🔌

Solid: arista por lectura

Las props son getters. No se re-ejecuta nada del cuerpo; cada lectura reconecta con la fuente y trae el valor vigente en ese instante.

💡
Rastreado depende de dónde lees, no de qué lees

La misma expresión props.valor se comporta distinto según el contexto. Dentro del JSX, de un efecto o de un memo, crea una suscripción. En el cuerpo del componente, fuera de todo scope reactivo, solo obtienes el valor de ese instante, sin suscribir nada. La reactividad no vive en la prop: vive en quien la lee.

El valor cambia con el tiempo

Interioriza esta frase: una prop es una expresión, no un dato. Cada lectura la reevalúa, así que dos lecturas en momentos distintos pueden devolver resultados distintos. Esto habilita el patrón más natural de Solid: derivar de las props sin ceremonia alguna.

function Precio(props: { base: number; iva: number }) {
  const total = createMemo(() => props.base * (1 + props.iva));
  return <span>{total()}</span>;
}

El memo se suscribe a props.base y a props.iva; cuando el padre mueva cualquiera de los dos, el getter correspondiente devolverá el nuevo número y el memo se recomputará. Si en cambio hubieras leído las props una sola vez en el cuerpo y guardado el resultado en una variable, ese resultado sería un fósil del primer render: correcto al montar, mentira desde el primer cambio.

// MAL: se calcula una vez en el cuerpo y muere ahí
const totalMuerto = props.base * (1 + props.iva);
// BIEN: la derivación se reevalúa con cada cambio de las fuentes
const total = createMemo(() => props.base * (1 + props.iva));

Existe también la operación inversa, para las raras veces en que quieres el valor actual sin suscribirte a sus cambios: untrack. Escribir untrack(() => props.valor) lee el getter una vez y no crea arista, útil para inicializar algo a partir del valor de arranque sin atarte a su evolución. Es la excepción que confirma la regla: por defecto, leer suscribe; desengancharse es un acto explícito y deliberado. Y ojo con confundirlo con destructurar: untrack sigue leyendo a través del getter en el momento en que lo llamas —te da el valor de ahora—, mientras que destructurar en el cuerpo te ata al valor del montaje. Uno elige cuándo leer sin suscribir; el otro, sin querer, lee demasiado pronto y para siempre.

ℹ️
En React el problema es el opuesto: closures rancios

Una función de React que captura props.valor congela el valor de ese render dentro de su closure; para ver el valor nuevo necesitas que el componente se re-ejecute y recree la función. Solid invierte el problema de raíz: el componente no se re-ejecuta jamás, así que no existen closures rancios, pero a cambio debes leer siempre a través del getter para que el valor sea el de ahora. Dos modelos, dos disciplinas opuestas: en React vigilas cuándo se recrea tu closure; en Solid vigilas que no separes el valor de su getter.

flowchart TD
A[Signal en el padre] --> B[Getter de la prop en el hijo]
B --> C[Lectura en JSX efecto o memo]
C --> D[Suscripcion fina al signal]
A -->|cambia| E[El getter reevalua]
E --> F[Solo ese consumidor se actualiza]
style A fill:#89b4fa,color:#11111b
style D fill:#a6e3a1,color:#11111b
style F fill:#cba6f7,color:#11111b

Getters de verdad: cuándo hay un Proxy

En el caso común, el objeto de props que recibes es un objeto normal con propiedades getter, como el que genera el compilador. Pero en cuanto entran en juego mergeProps (7.3) o splitProps (7.4), lo que recibes es un Proxy real: intercepta cada acceso y decide en vivo de qué fuente sale el valor. Por eso a este modelo se le llama “proxy reactivo de props”. La lección práctica es la misma en ambos casos: nunca supongas que tienes un valor guardado; asume siempre que estás mirando por una ventana que da a la fuente, y que la ventana solo muestra la verdad mientras la miras.

Este matiz tiene una consecuencia práctica y elegante: como esos proxies implementan bien las trampas de enumeración, el compilador puede hacerles spread —{...otras}— y seguir viendo cada propiedad como el getter que es. Un objeto plano nacido de una destructuración no tendría esas trampas: volvería a ser una foto. El proxy no es un adorno de implementación; es justo lo que sostiene la promesa de que reenviar props es reenviar aristas, no copiar datos. Cuando lo entiendes, dejas de preguntarte si “esto es reactivo” y aprendes a mirar cómo accedes: si accedes a través de un getter vivo, lo es; si lo has copiado a una variable plana, ya no.

Una prop es una arista del grafo, no una casilla de memoria

El salto mental definitivo es dejar de ver props como una bolsa de datos y empezar a verla como un haz de conexiones al grafo reactivo del padre. Cada propiedad dinámica es una arista: un canal por el que un signal de arriba se hace legible aquí abajo, y que se activa en el instante exacto de la lectura. Esto explica de golpe todo lo raro que Solid trae respecto a React. Explica por qué el componente puede correr una vez y aun así actualizarse: la vida no está en el cuerpo, está en las aristas que el cuerpo tendió. Explica por qué no hay comparación de props ni memo de componente: no se compara nada, se propaga por el grafo. Y anticipa —esto llega en 7.2— por qué destructurar es catastrófico: separar el getter de su objeto es cortar la arista y quedarte con el último valor muerto que devolvió. En el nuevo núcleo reactivo de Solid 2.0 cambia el motor de propagación, pero este contrato no se toca: leer una prop sigue siendo reconectarte, ahora, con la fuente. Cuando dejas de leer props.x como “el campo x” y lo lees como “reconéctame ya con la fuente de x”, has entendido el modelo de props de Solid.

⚔️ Comprueba que la prop respira
  1. Crea App con un signal n y un botón que lo incremente; renderiza <Visor valor={n()} />.
  2. En Visor, pon un createEffect que imprima props.valor y confirma que se dispara en cada clic.
  3. Ahora, en el cuerpo de Visor, lee const v = props.valor y muestra {v} en el JSX. Observa que queda congelado: leíste el getter una vez, fuera de tracking.
  4. Deriva const doble = createMemo(() => props.valor * 2) y comprueba que vuelve a fluir con el signal.
  5. Explica con tus palabras por qué el paso 3 falla y el 4 funciona, usando la idea de “arista del grafo”.