Saltar al contenido

Estado optimista: pintar el resultado antes de la confirmación

El estado optimista en Solid no se parchea a mano: se deriva. La colección de envíos en vuelo que da `useSubmissions` es una fuente reactiva, y su `input` contiene los datos que aún no confirmó el servidor; pintar esos datos como ítems provisionales junto a los reales de `createAsync` da la respuesta inmediata. Cuando la action termina, la query revalida en una transición y el dato real releva al fantasma sin parpadeo. Cuando falla, el envío deja de estar pending y el fantasma desaparece solo: la reversión no es un camino de código, es la ausencia de la proyección.

La idea: pintar desde el envío en vuelo

El material del optimismo ya está sobre la mesa: useSubmissions te da la colección de envíos activos, y cada uno guarda en input los datos con que se disparó. Un envío en vuelo es, literalmente, «una nota que el usuario ya mandó pero el servidor aún no confirmó». Pintar su input como un ítem provisional —un fantasma— es mostrar el futuro probable. No necesitas una segunda lista de estado optimista: la lista de envíos pendientes es esa lista.

TSX
import { For, Show } from "solid-js";import { createAsync, useSubmissions } from "@solidjs/router";function Notas() {  const notas = createAsync(() => obtenerNotas(), { initialValue: [] });  const creando = useSubmissions(crearNota);  return (    <ul>      <For each={notas()}>        {(n) => <li>{n.texto}</li>}      </For>      <For each={creando}>        {(envio) => (          <Show when={envio.pending}>            <li class="fantasma">{String(envio.input[0].get("texto"))}</li>          </Show>        )}      </For>    </ul>  );}

Los ítems reales salen de notas(), la fuente de verdad servida por la query; los fantasmas salen de creando, los envíos en vuelo. envio.input[0] es el FormData que se envió, y de él lees el texto que el usuario acaba de escribir. El usuario ve su nota aparecer al instante, aunque el servidor todavía esté trabajando.

El relevo sin parpadeo

La pregunta obvia es qué pasa en el instante de la confirmación: ¿no se verá un parpadeo cuando el fantasma desaparezca y el ítem real aparezca, o peor, un momento con los dos? La respuesta está en cómo Solid coordina el cierre del envío con la revalidación. Al terminar la action, la revalidación de la query corre dentro de una transición: el dato nuevo se prepara antes de aplicarse, y el cambio de la query y la baja del envío se aplican de forma coordinada. El resultado es un relevo limpio —el ítem real ya está listo cuando el fantasma se retira—, sin el destello de duplicado que tendrías si gestionaras ambas listas a mano.

Para borrados, la proyección es la inversa: en vez de añadir un fantasma, ocultas el ítem que un envío pendiente pretende eliminar. Compruebas si algún envío de borrado en vuelo apunta a ese id y, si es así, no lo pintas.

TSX
const borrando = useSubmissions(borrarNota);const estaBorrando = (id: string) =>  borrando.some((e) => e.pending && e.input[0] === id);<For each={notas()}>  {(n) => (    <Show when={!estaBorrando(n.id)}>      <li>        {n.texto}        <form action={borrarNota.with(n.id)} method="post">          <button type="submit">Borrar</button>        </form>      </li>    </Show>  )}</For>

Como borrarNota.with(n.id) prefija el id, ese id queda en input[0] del envío, y estaBorrando lo compara para esconder la fila en cuanto el usuario pulsa Borrar. El ítem desaparece al instante; cuando el servidor confirma, la query revalida sin él y la ocultación provisional se vuelve permanente.

La reversión es la ausencia del fantasma

Aquí está la joya del modelo. En un enfoque imperativo, «revertir si falla» es un camino de código explícito: guardas el estado anterior, aplicas el cambio optimista, y en el catch restauras lo guardado. Es la parte que más se olvida y peor se prueba. En Solid no existe ese camino porque el fantasma nunca fue un estado que modificaras: era una proyección del envío pendiente. Si la action falla, el envío deja de estar pending —pasa a tener error—, y como el fantasma solo se pintaba mientras pending era cierto, desaparece por sí solo. La lista real, que nunca tocaste, queda exactamente como estaba. La reversión no se ejecuta: simplemente el fantasma deja de existir.

TSX
<For each={creando}>  {(envio) => (    <>      <Show when={envio.pending}>        <li class="fantasma">{String(envio.input[0].get("texto"))}</li>      </Show>      <Show when={envio.error}>        <li class="fallida">          No se pudo guardar «{String(envio.input[0].get("texto"))}»          <button onClick={() => envio.retry()}>Reintentar</button>        </li>      </Show>    </>  )}</For>

Como el input sobrevive al fallo, puedes incluso ofrecer un retry() que reintente con los mismos datos, o mostrar la entrada fallida en rojo para que el usuario decida. Todo sale del mismo objeto de envío; no hay una copia de seguridad del estado que restaurar.