wandres.dev
CREATESIGNAL · el átomo reactivo

El setter: valor, actualizador y equals

Las dos formas de escribir en un signal —un valor directo o una función actualizadora setCount(c => c + 1)—, la trampa de los signals que guardan funciones, y la opción equals que decide cuándo una escritura re-notifica a los observadores y cuándo se descarta por ser igual.

⏱ 16 min

Si el getter es la mitad contemplativa del signal, el setter es la mitad que mueve el mundo: cada escritura recorre el grafo y despierta a los observadores. Pero escribir tiene más matices de los que parece —un valor directo o una función que transforma el anterior— y una decisión silenciosa gobierna cada escritura: ¿ha cambiado esto lo suficiente como para molestar a quien lo observa? Esa pregunta la responde equals.

🎯 Al terminar esta lección sabrás
  • Escribir con un valor directo y con una función actualizadora setCount(c => c + 1).
  • Saber cuándo el actualizador funcional es obligatorio y no un capricho de estilo.
  • Evitar la trampa de los signals que almacenan funciones.
  • Controlar con equals cuándo una escritura re-notifica y cuándo se descarta.

Dos formas de escribir

El setter acepta, o bien el nuevo valor directamente, o bien una función actualizadora que recibe el valor anterior y devuelve el siguiente. Ambas terminan estableciendo el estado; la diferencia es de dónde sale el valor nuevo.

const [count, setCount] = createSignal(10);

setCount(20);           // valor directo -> 20
setCount(c => c + 1);   // actualizador  -> recibe 20, devuelve 21
count();                // 21

La forma con función no es adorno. Es la correcta siempre que el valor nuevo dependa del anterior, porque garantiza que operas sobre el estado más reciente sin volver a leer el getter. Esto importa especialmente al agrupar varias escrituras o en código asíncrono, donde una lectura previa de count() podría haber quedado obsoleta:

// Fragil: lee count() una vez y suma sobre esa foto
setCount(count() + 1);
setCount(count() + 1);  // si count() se capturo antes, ambos parten de lo mismo

// Robusto: cada actualizador ve el resultado del anterior
setCount(c => c + 1);
setCount(c => c + 1);   // +2 garantizado

El setter, además, devuelve el valor recién establecido, lo que a veces resulta cómodo para registrar o encadenar: const nuevo = setCount(c => c + 1).

La trampa de guardar funciones

De la existencia del actualizador funcional se deriva un peligro concreto y desconcertante. Si tu signal guarda una función —un callback, un manejador—, al hacer setCallback(miFuncion) Solid interpretará miFuncion como un actualizador y la ejecutará pasándole el valor anterior, en lugar de guardarla. El estado acabará siendo lo que esa función devuelva, no la función misma.

const [callback, setCallback] = createSignal(() => "hola");

setCallback(otraFuncion);        // MAL: ejecuta otraFuncion(valorPrevio)
setCallback(() => otraFuncion);  // BIEN: el actualizador DEVUELVE la funcion
⚠️
Un signal de funciones necesita el doble arrow

Siempre que el tipo almacenado sea una función, escribir en él exige la forma setSignal(() => valorFuncion). El actualizador externo devuelve tu función sin ejecutarla. Es uno de los tropiezos más difíciles de diagnosticar en Solid porque no da error: simplemente guarda algo que no esperabas. Si un signal de callbacks “pierde” su función, sospecha de esto primero.

equals: la decisión de re-notificar

Cada vez que escribes, Solid compara el valor nuevo con el actual y solo notifica a los observadores si son distintos. La comparación por defecto es la igualdad estricta, equivalente a ===. Si estableces el mismo primitivo que ya había, la escritura es un no-op silencioso: ningún efecto se re-ejecuta, ningún memo recalcula.

const [count, setCount] = createSignal(5);

createEffect(() => console.log("valor:", count()));

setCount(5);   // igual que antes -> NO re-ejecuta el efecto
setCount(6);   // distinto -> el efecto corre

Esto es casi siempre lo que quieres: evita trabajo inútil. Pero tiene una cara incómoda con los objetos, porque === compara referencias, no contenido. Si mutas un objeto y vuelves a establecer la misma referencia, Solid concluye que no ha cambiado y no notifica:

const [user, setUser] = createSignal({ nombre: "Ada" });

const u = user();
u.nombre = "Alan";
setUser(u);        // misma referencia -> NO notifica: nadie se entera

setUser({ ...u }); // referencia nueva -> SI notifica

La opción equals te da el control. Puedes desactivar la comparación por completo con equals: false para que toda escritura notifique, aunque el valor sea idéntico —útil para señales que actúan como disparadores o que envuelven datos mutables—, o pasar tu propia función de igualdad:

// Notifica siempre, incluso con el mismo valor:
const [ping, setPing] = createSignal(0, { equals: false });

// Igualdad a medida: notifica solo si cambia el id
const [item, setItem] = createSignal(dato, {
  equals: (prev, next) => prev.id === next.id,
});
🟰

equals por defecto

Compara con ===. Escribir el mismo valor primitivo no notifica. La opción sensata para la inmensa mayoría de signals.

🚫

equals a false

Desactiva la comparación: cada escritura notifica. Para disparadores y para valores mutables que reutilizan la misma referencia.

🎯

equals a medida

Una función (prev, next) => boolean. Devuelve true para no notificar. Te deja comparar por contenido, por id o por lo que quieras.

El setter no propaga cambios, propaga diferencias

Hay una asimetría hermosa en el diseño del setter que conviene mirar de frente. Ingenuamente uno cree que escribir en un signal “avisa a los observadores”; en realidad, escribir ofrece un valor y es la función equals quien decide si ese valor constituye un cambio digno de propagarse. El sistema reactivo no transporta escrituras, transporta diferencias: si lo que entra es igual a lo que había, el pulso muere en el setter y el grafo entero se ahorra el trabajo. Esta frontera, la de la igualdad, es uno de los mandos más infravalorados de Solid. Por defecto es ===, una elección pragmática que hace lo correcto con números y cadenas y que, con objetos, te empuja con firmeza hacia la inmutabilidad: como comparar referencias significa que mutar-y-reestablecer no notifica, el lenguaje te sugiere crear objetos nuevos, que es justo lo que quieres para razonar sobre el estado. Pero cuando la inmutabilidad estorba —un buffer que reescribes, un evento que solo marca “algo pasó”, una estructura enorme que sería absurdo clonar— tienes la válvula de escape de equals: false, que convierte el signal en un disparador puro que notifica siempre. Y entre ambos extremos vive la igualdad a medida, con la que enseñas al grafo qué significa “cambiar” para tu dato: quizá solo el id, quizá una versión, quizá una comparación profunda. Elegir bien esta función es elegir cuánto trabajo hace tu aplicación. Quien la ignora acepta el === por defecto y casi siempre acierta; quien la domina afina la propagación del grafo hasta el último recálculo. El setter, en el fondo, no es una orden: es una propuesta sometida a la aprobación de equals.

⚔️ Domina la escritura
  1. Incrementa un signal dos veces seguidas con setCount(count() + 1) y luego con setCount(c => c + 1); explica por qué solo la segunda forma garantiza +2 bajo batch.
  2. Crea un signal que guarde una función y compruébalo: setFn(otra) frente a setFn(() => otra). Observa cuál guarda la función y cuál la ejecuta.
  3. Muta un objeto y reestablécelo con la misma referencia; confirma que el efecto no reacciona. Arréglalo esparciendo { ...obj }.
  4. Declara un signal con equals: false y escribe el mismo valor varias veces; verifica que el efecto se dispara en cada escritura.
  5. Escribe un equals a medida que solo notifique cuando cambie el campo id de un objeto y demuéstralo con dos escrituras.