Cuanto más compleja sea la aplicación frontend, más elementos diferentes contendrá. Cada uno de estos elementos reacciona de alguna manera a lo que sucede a su alrededor: los spinners giran, los botones se desactivan, los menús aparecen y desaparecen, los datos se envían.
Idealmente, cualquier cambio en la interfaz es el resultado de un cambio en los datos, es decir, en el estado de la aplicación. Imagina un formulario de registro en el que el botón de envío (submit) está desactivado mientras se realiza una solicitud al servidor (esto es obligatorio desde el punto de vista de la experiencia de usuario para cualquier formulario). En este caso, el estado puede tener el siguiente aspecto:
const state = {
registrationProcess: {
valid: true,
submitDisabled: true,
isLoading: false,
}
};
En aplicaciones reales, la situación es aún más compleja. Durante el envío de datos, no solo se bloquea el botón de envío, sino también el campo de entrada. Además, el envío de datos en un lugar puede afectar a otros bloques en la página, que pueden desaparecer, bloquearse o modificarse. Sin mencionar que puede haber varias razones para bloquear el botón. Puede estar bloqueado simplemente porque se han ingresado datos incorrectos en el formulario.
Si abordamos este problema de manera directa, el estado resultante tendrá muchos indicadores:
const state = {
registrationProcess: {
valid: true,
submitDisabled: true,
inputDisabled: true,
showSpinner: true,
blockAuthentication: true,
}
};
Cada indicador se refiere a su elemento en la pantalla. A medida que aumenta el número de indicadores, la lógica de actualización del estado se vuelve más complicada (es necesario coordinarlos entre sí y recordar actualizarlos) y la lógica de visualización se vuelve más compleja (aparecen diferentes dependencias de la visualización externa en función de los diferentes indicadores).
El problema de este enfoque es que se basa en las consecuencias de lo que está sucediendo, no en las causas. El cambio en la actividad del botón, el bloqueo de los elementos, la visualización de los spinners, todo esto son consecuencias de algún proceso. La capacidad de identificar estos procesos y describirlos correctamente en el estado es una de las piedras angulares de una buena arquitectura.
En el ejemplo anterior, la mayoría de los indicadores están relacionados con el proceso de procesamiento de los datos del formulario. Supongamos que después de enviar el formulario, los datos se envían al servidor, luego se recibe una respuesta y luego el resultado se muestra al usuario. El resultado puede ser exitoso o no exitoso. Debemos considerar todos los posibles resultados. El proceso completo se puede dividir en varios estados intermedios:
Este conjunto propuesto no es universal. Los procesos pueden ser más complejos, lo que requeriría un conjunto diferente de estados. Y los nombres de los estados son participios.
- filling – completando el formulario. En este estado, todo está activo y disponible para su edición.
- processing (o sending) – enviando el formulario. Este es el estado en el que el usuario está esperando y la aplicación intenta evitar acciones no deseadas, como hacer clic o cambiar los datos del formulario.
- processed (o finished) – estado que indica que todo ha terminado. En este estado, el formulario ya no se muestra.
- failed – estado que indica una finalización con error. Por ejemplo, hubo un fallo en la red durante la carga o los datos cargados resultaron ser incorrectos.
Desde el punto de vista de la teoría de autómatas (y estamos tratando con programación de autómatas en este caso), estos estados se llaman estados de control. Determinan dónde nos encontramos actualmente. Reescribamos nuestro estado:
const state = {
registrationProcess: {
state: 'filling', // 'processing', 'processed', 'failed'
}
};
En el estado, hemos identificado el proceso de registro, que puede tener uno de los posibles estados. Incluso este pequeño cambio a primera vista simplifica drásticamente el sistema. Ahora no necesitamos rastrear cada elemento involucrado en este proceso. Lo más importante es que todos los posibles estados describan todas las posibles formas de comportamiento. Entonces, todas las comprobaciones en la visualización se reducirán a comprobar el estado general:
// Puede haber tantos "if" como sea necesario,
// lo importante es que estén basados en el estado general, no en comprobaciones de
// indicadores específicos
if (state.registrationProcess.state === 'processing') {
// Bloquear botones
// Activar spinners
}
if (state.registrationProcess.state === 'failed') {
// Mostrar mensaje de error
}
Además de estos estados, hay varios datos diferentes que acompañan a nuestro proceso. Por ejemplo, processed puede terminar con errores. En este caso, podemos agregar una matriz (array) o un objeto, dependiendo de la estructura de errores que se llenará si hay errores:
const state = {
registrationProcess: {
errors: ['El nombre no está completo', 'La dirección tiene un formato incorrecto'],
state: 'processed',
}
};
Además, esta misma matriz de errores se puede utilizar para validar el formulario antes de enviarlo al servidor. Es decir, cuando estamos en el estado filling.
¿Y qué pasa si queremos bloquear la posibilidad de enviar el formulario hasta que se haya validado en el frontend? Hay dos enfoques: o bien comprobamos que errors esté vacío, o mejor aún, introducimos un estado explícito de validez del formulario. Entonces, el estado de nuestra aplicación se vería así:
const state = {
registrationProcess: {
errors: ['El nombre no está completo', 'La dirección tiene un formato incorrecto'],
state: 'processed',
validationState: 'invalid' // o valid
}
};
En algunas situaciones, es posible combinar la validación con el proceso de registro en sí. En lugar de tener un estado separado validationState, aparecería un estado adicional invalid dentro de state. Esto no es del todo correcto desde el punto de vista de la modelización (porque realmente tenemos dos procesos diferentes), pero a veces este enfoque permite escribir un código un poco más simple (hasta que haya muchas diferencias).
En general, este enfoque de desarrollo se llama programación con estado explícito. Se reduce a que dentro de la aplicación hay procesos básicos que afectan todo lo demás. Luego, estos procesos se modelan utilizando autómatas finitos (FSM). No importa qué herramientas se utilicen para el desarrollo: DOM puro, jQuery o cualquier framework moderno y potente. Es aplicable en todas partes y es necesario en todas partes.
Materiales adicionales
Para acceder completo a curso necesitas un plan básico
El plan básico te dará acceso completo a todos los cursos, ejercicios y lecciones de Códica, proyectos y acceso de por vida a la teoría de las lecciones completadas. La suscripción se puede cancelar en cualquier momento.