El arranque: el estado que aún no sabe
El parpadeo entre el formulario de entrada y el contenido no es un problema de estilos ni de velocidad de red: es un error de modelado que consiste en inicializar una respuesta antes de haber hecho la pregunta. Esta lección desarrolla el estado de comprobación como tercera situación epistémica irreductible, con su subdivisión entre espera imperceptible y espera larga, su plazo máximo, su destino guardado para retomar la navegación interrumpida y su relación con el renderizado en servidor, donde la hidratación convierte el parpadeo en una discrepancia estructural.
Hay un defecto que aparece en casi todas las aplicaciones con sesión y que ningún usuario reporta porque dura doscientos milisegundos: al recargar, la pantalla enseña el formulario de entrada durante un instante y luego salta al contenido, o al revés. Se le suele llamar parpadeo y se le suele tratar como un asunto cosmético que se resuelve con un retardo, una opacidad o un truco de renderizado. No lo es. El parpadeo es la manifestación visible de una mentira en el modelo: alguien inicializó una variable llamada estaAutenticado con el valor falso, que significa no hay sesión, cuando lo cierto en ese instante era todavía no lo sé. La distancia entre esas dos frases es exactamente el estado que falta, y darle nombre no solo cura el parpadeo, sino toda una familia de defectos de arranque que rara vez se relacionan con él.
- Diagnosticar el parpadeo de arranque como un valor inicial que finge conocimiento.
- Modelar la comprobación como estado propio con subdivisión entre espera corta y espera larga.
- Acotar el arranque con un plazo máximo y un destino de fallo que no bloquee la aplicación.
- Conservar el destino de navegación interrumpido para retomarlo cuando la sesión se confirme.
El valor inicial que miente
Toda variable booleana necesita un valor de partida, y en autenticación ese valor se elige casi siempre por descarte: como todavía no hay usuario, se inicializa a falso. La elección parece inofensiva y contiene un salto lógico grave, porque confunde la ausencia de información con la información de ausencia. Un tipo booleano solo tiene dos habitantes y el arranque necesita tres, así que el modelo obliga a mentir desde la primera línea, y todo lo que se construye encima hereda la mentira: el enrutador que decide a dónde ir, el componente que decide qué pintar y la petición inicial que decide si incluir credenciales.
La consecuencia inmediata es el parpadeo, pero no es la más grave. La más grave es la carrera: mientras la comprobación viaja por la red, la aplicación está funcionando con una respuesta provisional que se comporta como definitiva. El enrutador puede haber redirigido ya a la pantalla de entrada y haber borrado del historial la dirección a la que el usuario iba; un componente puede haber lanzado una petición sin credenciales que el servidor rechazará; un efecto puede haber limpiado un borrador guardado porque nadie estaba autenticado. Cuando la comprobación termina y corrige el valor, esas decisiones ya se tomaron y ninguna se deshace sola.
// El modelo que produce el parpadeo
let usuario: Usuario | null = null
let cargando = false // arranca en falso, aunque va a cargar
// El modelo que lo hace imposible
type Arranque =
| { fase: 'comprobando' }
| { fase: 'anonimo' }
| { fase: 'autenticado'; usuario: Usuario }
La unión discriminada de la derecha ya es una máquina de estados, solo que sin transiciones. Escribirla como tal en el grafo del nivel es dar el paso que faltaba: convertir comprobando en un estado inicial real del que solo se sale por la respuesta de la restauración, y con ello garantizar que ninguna parte de la aplicación pueda observar una decisión sobre la sesión antes de que exista.
Falso desde el principio
El booleano inicializado a falso afirma que no hay sesión cuando lo cierto es que nadie ha preguntado todavía.
Redirección prematura
El enrutador decide con la respuesta provisional y borra del historial el destino al que iba el usuario.
Petición sin credencial
Un componente montado durante la ventana pide sus datos sin sesión y recibe un rechazo que nadie esperaba.
Limpieza indebida
Un efecto observa que no hay usuario y borra un borrador que pertenecía a la sesión que estaba a punto de restaurarse.
Las cuatro consecuencias comparten una propiedad incómoda: ninguna produce un error visible en el momento en que ocurre. Todas dejan la aplicación en un estado plausible que se corrige un instante después, y por eso sobreviven años en producción disfrazadas de rareza ocasional. La única forma de detectarlas es ralentizar la comprobación a propósito y observar qué decide el sistema mientras no sabe nada.
La prueba definitiva de que falta un estado es contar los habitantes que el dominio necesita y compararlos con los que el tipo ofrece. La sesión en arranque necesita tres: desconocida, ausente y presente. Cualquier codificación con un solo booleano tendrá que elegir cuál de las tres sacrifica, y sacrificará siempre la desconocida porque es la que dura menos tiempo. Ese es justamente el error: la brevedad de un estado no dice nada sobre su importancia, y este dura poco pero gobierna la primera decisión que toma la aplicación.
Una espera que se ve y una que no
Resuelto el modelo, aparece un problema de experiencia que también es de modelado. Si la comprobación tarda cuarenta milisegundos, mostrar un indicador de carga produce un destello tan molesto como el parpadeo que veníamos a curar; si tarda tres segundos y no mostramos nada, el usuario ve una pantalla muerta y vuelve a pulsar. La respuesta correcta no es elegir un mal menor sino reconocer que hay dos esperas distintas y que la diferencia entre ellas es, otra vez, un estado.
comprobando: {
initial: 'silencioso',
invoke: {
src: 'restaurar',
onDone: [
{ target: 'autenticado', guard: 'haySesion', actions: 'guardarSesion' },
{ target: 'anonimo' },
],
onError: 'anonimo',
},
after: { LIMITE_ARRANQUE: 'degradado' },
states: {
silencioso: { after: { UMBRAL_VISIBLE: 'visible' } },
visible: {},
},
},
degradado: {
on: { REINTENTAR: 'comprobando', ENTRAR: 'autenticando' },
},
El subestado silencioso cubre los primeros trescientos milisegundos y no pinta nada, porque una espera imperceptible no merece ser anunciada. Si la respuesta llega dentro de esa ventana, el usuario nunca ve un indicador y percibe la aplicación como instantánea. Si no llega, la máquina pasa a visible y ahí sí aparece el esqueleto o el rótulo de carga, ya con la certeza de que la espera es real y de que anunciarla ayuda en lugar de estorbar. Nótese que la invocación es la misma en ambos subestados: vive en el padre, así que el cambio de subestado no la reinicia ni la duplica.
El plazo máximo del padre es la pieza que casi nadie escribe y la que evita el peor fallo de arranque posible, que es la aplicación colgada para siempre en su pantalla de carga porque la comprobación nunca respondió ni falló. Una red que no cierra la conexión, un intermediario que traga la petición o un servidor saturado producen exactamente eso, y sin un plazo la máquina se queda esperando un evento que no llegará. El estado degradado transforma un bloqueo indefinido en una situación con salida: la aplicación admite que no pudo determinar la sesión, ofrece reintentar y permite entrar manualmente.
stateDiagram-v2
[*] --> comprobando
state comprobando {
[*] --> silencioso
silencioso --> visible: pasa el umbral perceptible
}
comprobando --> autenticado: sesion restaurada
comprobando --> anonimo: sin sesion
comprobando --> degradado: vence el plazo maximo
degradado --> comprobando: REINTENTAR
degradado --> autenticando: ENTRARSuspender la decisión no significa suspender la aplicación entera. La cabecera, la navegación pública, la portada y cualquier contenido que se vea igual con sesión y sin ella pueden renderizarse durante comprobando sin riesgo, y hacerlo mejora la percepción de velocidad de forma notable. Lo que debe esperar es únicamente lo que cambia según la respuesta: las zonas privadas, los elementos que dependen del usuario y las redirecciones. La regla es sencilla: bloquea las decisiones, no los píxeles.
Recordar a dónde iba
El arranque tiene una segunda responsabilidad que suele quedar huérfana entre el enrutador y la máquina. Cuando alguien abre un enlace profundo a una pantalla privada y su sesión debe restaurarse primero, la dirección original es información valiosa que se pierde en cuanto el enrutador redirige. Guardarla en el contexto en el momento exacto en que la máquina descubre que necesita comprobar, y consumirla cuando la sesión se confirma, convierte una frustración clásica en un detalle invisible de calidad.
entry: assign({
destino: ({ context }) => context.destino ?? rutaActual(),
}),
La forma de escribirlo importa: el destino se fija solo si aún no había uno, para que una redirección intermedia no lo pise, y se limpia al consumirlo para que no reaparezca en un arranque posterior. Conviene además validarlo antes de navegar, porque un destino que viene de la barra de direcciones es entrada no confiable y una redirección abierta es una vulnerabilidad real: acepta rutas internas de tu propia aplicación y descarta cualquier cosa que apunte fuera.
Hay un detalle de coordinación con el enrutador que decide si esto funciona o estorba. Si el enrutador tiene su propia protección de rutas y consulta a la máquina, debe entender comprobando como una respuesta legítima y no como una negativa: mientras la máquina esté ahí, la protección no permite ni deniega, simplemente aún no resuelve. Los enrutadores modernos expresan eso como una carga pendiente en la propia ruta, y esa es la integración correcta; la incorrecta es que el enrutador interprete la ausencia de usuario como prohibición y ejecute la redirección que después habrá que deshacer.
| Estrategia de credencial | Qué ocurre al arrancar | Coste del arranque |
|---|---|---|
| cookie de sesión del servidor | el navegador la envía sola, basta preguntar | una llamada, siempre |
| credencial en memoria | se pierde al recargar, hay que renovar | una llamada de refresco |
| credencial en almacenamiento local | se lee sin red, pero hay que validarla | lectura instantánea más verificación |
| renderizado en servidor con cookie | el servidor ya sabe y marca el estado inicial | ninguna, si la hidratación coincide |
La segunda fila explica por qué muchas aplicaciones que guardan la credencial solo en memoria parecen lentas al recargar sin serlo: no están comprobando nada, están renovando, y la renovación es una operación más cara que la consulta. Si esa es tu estrategia, el estado de comprobación debe invocar el refresco directamente en lugar de preguntar primero y renovar después, porque encadenar dos viajes duplica el arranque sin aportar información.
La última fila merece un comentario, porque es la que cambia el problema de sitio en lugar de resolverlo. Si el servidor renderiza sabiendo que hay sesión y el cliente arranca su máquina en comprobando, la primera hidratación produce una discrepancia entre lo que ya estaba pintado y lo que el cliente cree: el parpadeo vuelve, y esta vez acompañado de avisos en la consola. La solución es tratar el conocimiento del servidor como entrada de la máquina, arrancándola directamente en autenticado cuando el servidor lo confirmó, en lugar de obligarla a redescubrir en el cliente algo que ya se sabía.
Que el arranque confirme una sesión no significa que el usuario pueda hacer lo que va a intentar. La comprobación resuelve una pregunta de identidad, no de autorización, y confundirlas produce interfaces que muestran zonas completas a las que el servidor responderá con un rechazo. El arranque debe limitarse a establecer quién es el usuario y dejar que la capa de permisos, que es el asunto de la última lección de este nivel, decida qué le está permitido.
Todo sistema que consulta el mundo pasa por un intervalo en el que no sabe la respuesta, y la calidad de ese sistema se mide en buena parte por lo que hace durante ese intervalo. La tentación universal es rellenarlo con una suposición, porque una suposición permite seguir adelante sin cambiar el tipo de dato y el código compila igual; el precio es que el sistema empieza a actuar sobre una creencia que no tiene y que luego tendrá que retirar. Modelar la ignorancia explícitamente invierte esa economía: el compilador deja de aceptar los caminos que asumían conocimiento y obliga a decidir, en cada punto, qué se hace mientras no se sabe. Esa obligación es incómoda las primeras horas y es exactamente el trabajo de diseño que faltaba, porque revela cuántas decisiones de tu aplicación se estaban tomando con información inventada. El caso de la autenticación es el más visible por su brevedad, pero el patrón se repite en todas partes: los permisos antes de cargar el perfil, la configuración remota antes de su primera respuesta, la conectividad antes del primer intento, la geolocalización antes de que el usuario conteste al diálogo. En todos ellos el error tiene la misma forma, que es dar por respondida una pregunta pendiente, y la cura tiene la misma forma también, que es añadir el habitante que faltaba al tipo y dejar que las reglas se reescriban solas alrededor de él. Un sistema honesto sobre lo que ignora es un sistema que nunca tiene que desdecirse en pantalla.
- Ralentiza artificialmente la comprobación de sesión a dos segundos y observa qué decide tu aplicación durante ese intervalo.
- Sustituye el booleano inicial por una unión de tres habitantes y sigue los errores del compilador hasta que no quede ninguno.
- Divide la comprobación en espera silenciosa y espera visible, y ajusta el umbral midiendo tu tiempo real de restauración.
- Añade un plazo máximo con un destino degradado y verifica el comportamiento cortando la red a mitad del arranque.
- Guarda el destino interrumpido en el contexto, valida que sea una ruta interna y comprueba que un enlace profundo lleva al sitio correcto tras entrar.
- Si renderizas en servidor, pasa el conocimiento del servidor como entrada de la máquina y confirma que la hidratación ya no produce discrepancias.