equals: false y comparadores a medida
El tercer argumento de createSignal decide qué significa cambiar. Por defecto es una igualdad estricta, pero puedes forzar notificación en cada escritura con equals: false o imponer tu propia relación de equivalencia con un comparador que iguale por contenido, por id o por versión.
La firma: equals define qué es cambiar
createSignal acepta un tercer canal de configuración donde vive la decisión más infravalorada del primitivo. Su forma completa es esta:
const [get, set] = createSignal<T>(inicial, { equals: (prev, next) => prev === next, // el valor por defecto});equals es una función (prev, next) => boolean con una convención que conviene grabar del revés: devuelve true cuando considera que los valores son iguales, e igual significa no notifiques. La comparación por defecto es ===, la igualdad estricta por referencia. Para números, cadenas y booleanos es exactamente lo que quieres: reescribir 5 sobre 5 es un no-op silencioso y el grafo entero se ahorra el trabajo.
La lectura profunda es que un signal no expone el flujo bruto de escrituras, sino ese flujo cocientado por la relación que define equals. Dos escrituras que la relación juzga equivalentes colapsan en una sola desde el punto de vista de los observadores. Cambiar equals es redefinir esa relación de equivalencia y, con ella, la identidad misma del estado que el grafo percibe.
Conviene una precisión de nivel avanzado: el defecto es ===, no Object.is. La diferencia importa en dos valores traicioneros. NaN === NaN es false, así que un signal que guarda NaN y recibe otro NaN notifica, porque la igualdad estricta los considera distintos. Y -0 === 0 es true, de modo que escribir -0 sobre 0 se descarta como no-op. Rara vez muerden, pero cuando tu dominio incluye cálculos numéricos con estos casos límite, saber qué relación exacta usa el defecto te ahorra un depurado desconcertante.
equals: false, el signal como pulso
El extremo opuesto a comparar es no comparar. Con equals: false, toda escritura notifica, aunque el valor sea idéntico al anterior:
// Notifica en cada escritura, coincida o no el valor:const [ping, setPing] = createSignal(undefined, { equals: false });createEffect(() => { ping(); disparar(); });setPing(undefined); // el efecto corre igual, una y otra vezEsto degrada deliberadamente el signal a un disparador: cada escritura es un evento, no un cambio de estado. Sus usos legítimos son contados —reejecutar un efecto tras una acción idempotente, envolver un buffer mutable que reescribes en su sitio, contar pulsos de un latido— y todos comparten una firma: el acto de escribir importa más que el valor escrito.
El caso del buffer mutable ilustra por qué existe la válvula. Si guardas un Float32Array que reescribes en su sitio por rendimiento —clonarlo en cada fotograma sería absurdo— la referencia nunca cambia y el === por defecto nunca notificaría. Aquí equals: false es legítimo: reconoces que el contrato de inmutabilidad no aplica y le pides al grafo que confíe en el acto de escritura como señal de cambio.
// Un buffer que reescribes en su sitio: la referencia no cambia nuncaconst [muestras, setMuestras] = createSignal(buffer, { equals: false });llenarEnSitio(buffer); // muta el mismo Float32ArraysetMuestras(buffer); // con equals: false, notifica pese a la misma referenciaComparadores a medida: igualar por contenido
Entre el === estricto y el equals: false promiscuo vive el territorio más rico: enseñarle al grafo qué significa cambiar para tu dato concreto. Un signal que guarda objetos con === notifica en cuanto cambia la referencia, aunque el contenido sea idéntico; un comparador a medida corta ese ruido igualando por lo que de verdad importa.
type Item = { id: string; nombre: string; version: number };// Notifica solo si cambia la version; ignora reasignaciones equivalentes:const [item, setItem] = createSignal<Item>(inicial, { equals: (prev, next) => prev.version === next.version,});// Igualdad superficial por claves:const superficial = <O extends Record<string, unknown>>(a: O, b: O) => { const ka = Object.keys(a); return ka.length === Object.keys(b).length && ka.every((k) => a[k] === b[k]);};const [perfil, setPerfil] = createSignal(dato, { equals: superficial });Comparar por id, por version o por un hash es casi siempre superior a una comparación estructural profunda: te da la potencia de igualar por contenido pagando un coste constante en lugar de recorrer el objeto entero. Si tu fuente emite referencias nuevas con contenido repetido —una respuesta de red que revalidas, un map que reconstruye la lista— un comparador barato absorbe esas coincidencias sin despertar a nadie.
El comparador corre en cada escritura
Un comparador no es gratis: se ejecuta en cada intento de escritura, se descarte o no el valor. Un equals que normaliza estructuras o recorre arrays grandes suma su coste al de cada set, y puede acabar costando más de lo que ahorra aguas abajo. La regla es estimar antes de imponer: ¿cuánto cuesta comparar frente a cuánto trabajo evitas si la comparación acierta?
Pero el peligro serio no es el rendimiento, es la corrección. Un comparador demasiado laxo —que devuelve true para valores que en realidad difieren— hace que Solid descarte actualizaciones legítimas: la UI se queda congelada mostrando un estado viejo y no hay error que lo delate. Igualar por id cuando el resto del objeto cambia es el caso clásico: cambias el nombre, mantienes el id, y la vista nunca se entera.
// PELIGRO: iguala por id, pero el nombre puede cambiar sin notificarconst [u, setU] = createSignal(user, { equals: (a, b) => a.id === b.id });setU({ ...user, nombre: "otro" }); // mismo id -> la vista NO se actualizaDe ahí una asimetría de riesgo que conviene grabar: los dos errores no cuestan lo mismo. Un comparador demasiado estricto —que distingue de más— solo desperdicia trabajo: notifica cuando no hacía falta, y lo peor que pasa es un recálculo de sobra. Un comparador demasiado laxo —que iguala de más— corrompe datos: silencia cambios reales y muestra estado obsoleto sin dejar rastro. Ante la duda, peca siempre por el lado estricto, porque el coste de una notificación de más es visible y acotado, mientras que el de una de menos es invisible y se paga en horas de depurado sobre un síntoma que ni siquiera parece reactivo.