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