batch: una sola propagación
batch agrupa varias escrituras para que memos y efectos se recalculen una sola vez al final, no una por cada setter. Cómo Solid ya auto-agrupa las escrituras que nacen dentro de una computación, por qué las escrituras asíncronas escapan a ese auto-batching, y en qué casos batch sigue siendo la diferencia entre una pasada y muchas.
Una escritura, un ciclo; muchas escrituras, un solo ciclo
batch recibe una función, ejecuta todas las escrituras que contenga y difiere la propagación hasta que la función termina. Los efectos y los memos que dependían de esos signals se recalculan una sola vez al final, con el estado ya completo, en lugar de una vez por cada setter. Devuelve lo que la función retorne: batch<T>(fn: () => T): T.
import { createSignal, createMemo, batch } from "solid-js";const [nombre, setNombre] = createSignal("Ada");const [apellido, setApellido] = createSignal("Lovelace");const completo = createMemo(() => { console.log("recalculo"); return `${nombre()} ${apellido()}`;});batch(() => { setNombre("Grace"); setApellido("Hopper");}); // "recalculo" se imprime UNA vez, no dosSin el batch, cada setter abriría y cerraría su propia pasada, y todo lo que dependa de ambos signals correría dos veces, observando de paso un estado intermedio absurdo —“Grace Lovelace”— que nunca fue real. Con batch, esa incoherencia transitoria no llega a existir: la interfaz salta del estado viejo al nuevo sin escalas.
Dentro del batch, los setters siguen aplicándose de inmediato para las lecturas directas de signals; lo que se aplaza hasta el cierre es la re-ejecución de los efectos y las re-renderizaciones. Es decir: no congelas los valores, congelas la notificación.
El auto-batching: el ciclo reactivo ya es un lote
Aquí está la clave que casi nadie explica bien. Solid ya agrupa solo en un caso importantísimo, y entender por qué te dice exactamente cuándo batch sobra.
Cuando un setter corre y no hay ningún ciclo de actualización en curso, Solid abre uno, aplica el cambio y, al cerrarlo, vacía la cola de efectos. Pero si el setter corre mientras un ciclo ya está abierto, la escritura se une al ciclo en marcha en vez de abrir el suyo. ¿Y cuándo hay un ciclo abierto? Precisamente cuando escribes desde dentro de una computación: el cuerpo de un createEffect, de un createMemo, de un createComputed. La propia ejecución del sistema reactivo es un lote.
createEffect(() => { // ya estamos dentro de un ciclo: estas dos escrituras se auto-agrupan setNombre("Grace"); setApellido("Hopper"); // los efectos dependientes corren una vez al cerrar este ciclo});Por eso, dentro de efectos, escribir varios signals seguidos no multiplica las pasadas: el auto-batching lo cubre gratis. batch ahí sería redundante.
Leer dentro del lote
Un matiz que sorprende a quien piensa batch como una transacción congelada: dentro del lote, si lees un signal que acabas de escribir, obtienes el valor nuevo, no el viejo. Los setters aplican su valor de inmediato; lo único que se aplaza es la notificación a los efectos. Se difiere reaccionar, no escribir.
const [x, setX] = createSignal(1);batch(() => { setX(2); console.log(x()); // imprime 2: la escritura ya está aplicada setX(3); console.log(x()); // imprime 3}); // los efectos que dependen de x corren una vez, y ven 3Los memos son un caso intermedio que conviene tener claro: como son perezosos, leer un memo dentro del lote después de cambiar sus fuentes lo recalcula en el acto y te da el valor fresco. Lo que no ocurre hasta cerrar el batch es la re-ejecución de los efectos y el repintado del DOM. Dicho de otro modo, el lote te garantiza consistencia de lectura —siempre ves lo último— y solo aplaza el trabajo observable hacia afuera.
Y como batch devuelve lo que su función retorne, puedes agrupar y computar en una sola expresión, sin una variable temporal fuera del lote:
const resumen = batch(() => { setUsuario(u); setPermisos(p); return `${u.nombre}: ${p.length} permisos`; // este valor sale del batch});Los batch anidados no apilan capas: un batch dentro de otro se funde con el externo, porque ya hay un ciclo abierto y la escritura se une a él —la misma regla del auto-batching—. No existe, pues, el peligro de “sobre-agrupar” por composición: envolver de más es, como mucho, redundante, nunca incorrecto. Esto hace que batch sea seguro de aplicar en funciones reutilizables sin saber si el llamador ya está dentro de otro lote.
La frontera asíncrona: donde batch vuelve a importar
El auto-batching cubre las escrituras que nacen dentro del ciclo reactivo. Fuera de él, cada setter abre y cierra su propia pasada. Y hay un lugar donde siempre estás fuera del ciclo: los callbacks asíncronos. Un setTimeout, un .then de una promesa, la respuesta de un fetch, un mensaje de WebSocket, un requestAnimationFrame —todos corren después de que el ciclo original se cerró—.
async function cargar() { const datos = await fetch("/api").then((r) => r.json()); // fuera de todo ciclo reactivo: sin batch, cada setter propaga por separado batch(() => { setUsuario(datos.usuario); setPermisos(datos.permisos); setCargando(false); });}Sin el batch, esos tres setters disparan tres propagaciones, y un efecto que dependa de los tres correría tres veces, viendo estados a medio hidratar. Con batch, la interfaz pasa de “cargando” a “listo” en un solo salto coherente. Esta es la regla operativa: en código asíncrono que escribe varios signals relacionados, agrupa.