La gestión de estado en procesos que implican múltiples etapas puede ser un desafío. Formularios de registro con verificación telefónica, validación de email y configuración de contraseña, o flujos de compra que incluyen confirmación del carrito, aplicación de cupones y procesamiento de pagos, demandan una comunicación eficiente entre componentes. Las soluciones tradicionales suelen ser excesivamente complejas (bibliotecas de estado pesadas) o propensas a un código desorganizado (prop drilling). Este artículo explora cómo combinar la biblioteca Unstated con el patrón de diseño Cadena de Responsabilidad para simplificar estos escenarios.
Al finalizar, comprenderás cómo construir flujos de trabajo modulares y mantenibles utilizando contenedores de estado independientes que colaboran entre sí.
Unstated: Gestion del estado con simplicidad
Unstated es una biblioteca minimalista que facilita el estado compartido en aplicaciones React. Su esencia reside en tres elementos: Container, Provider y Subscribe. Con menos de 200 líneas de código, ofrece una alternativa ligera a herramientas como Redux, ideal para aplicaciones de pequeña y mediana escala.
import { Container, Provider, Subscribe } from 'unstated';
class ContadorContainer extends Container {
state = { valor: 0 };
incrementar = () => this.setState({ valor: this.state.valor + 1 });
}
function BotonContador() {
return (
<Subscribe to={[ContadorContainer]}>
{contador => (
<button onClick={contador.incrementar}>{contador.state.valor}</button>
)}
</Subscribe>
);
}
function App() {
return (
<Provider>
<BotonContador />
</Provider>
);
}
El patrón Cadena de Responsabilidad para procesos multi-paso
Los flujos multi-paso presentan características comunes: dependencias secuenciales (ej. paso A debe completarse antes del paso B), estado compartido que viaja entre etapas y la posibilidad de avanzar o retroceder. El patrón Cadena de Responsabilidad, implementado con Unstated, descompone el proceso en contenedores de estado autónomos que se encadenan mediante inyección de dependencias.
| Concepto del patrón | Implementación con Unstated |
|---|---|
| Manejador abstracto | Clase que extiende Container |
| Manejador concreto | Subclase de Container con lógica específica del paso |
| Cliente | Componente usando Subscribe y Provider |
| Cadena | Inyección de contenedores en el coordinador del flujo |
Ejemplo práctico: Formulario de registro escalonado
1. Contenedores de estado por paso
// Contenedor para la verificación telefónica
class VerificarTelefonoContainer extends Container {
state = { telefono: '', codigo: '', verificado: false };
async verificar() {
const resultado = await api.verificarTelefono(this.state.telefono, this.state.codigo);
this.setState({ verificado: resultado.exitoso });
return resultado.exitoso;
}
}
// Contenedor para el establecimiento de contraseña
class ContrasenaContainer extends Container {
state = { contrasena: '', confirmacion: '', valida: false };
validar() {
const esValida = this.state.contrasena === this.state.confirmacion && this.state.contrasena.length >= 8;
this.setState({ valida: esValida });
return esValida;
}
}
2. Contenedor coordinador de la cadena
class FlujoRegistroContainer extends Container {
state = { pasoActual: 1, pasos: ['telefono', 'contrasena', 'perfil'] };
inyectarPasos(telefonoContainer, contrasenaContainer, perfilContainer) {
this.pasosContenedores = { telefonoContainer, contrasenaContainer, perfilContainer };
}
async siguientePaso() {
switch(this.state.pasoActual) {
case 1:
const telefonoValido = await this.pasosContenedores.telefonoContainer.verificar();
if (telefonoValido) this.setState({ pasoActual: 2 });
break;
case 2:
const contrasenaValida = this.pasosContenedores.contrasenaContainer.validar();
if (contrasenaValida) this.setState({ pasoActual: 3 });
break;
default:
console.warn('Flujo completado o paso no reconocido');
}
}
pasoAnterior() {
if (this.state.pasoActual > 1) {
this.setState({ pasoActual: this.state.pasoActual - 1 });
}
}
}
3. Componente de interfaz del flujo
function FormularioRegistro() {
return (
<Subscribe to={[FlujoRegistroContainer, VerificarTelefonoContainer, ContrasenaContainer]}>
{(flujo, telefono, contrasena) => (
<div className="registro-multipaso">
<IndicadorPasos pasos={flujo.state.pasos} actual={flujo.state.pasoActual} />
{flujo.state.pasoActual === 1 && (
<FormularioTelefono
telefono={telefono.state.telefono}
codigo={telefono.state.codigo}
onChange={(campo, valor) => telefono.setState({ [campo]: valor })}
onVerificar={() => flujo.siguientePaso()}
/>
)}
{flujo.state.pasoActual === 2 && (
<FormularioContrasena
contrasena={contrasena.state.contrasena}
confirmacion={contrasena.state.confirmacion}
onChange={(campo, valor) => contrasena.setState({ [campo]: valor })}
onSiguiente={() => flujo.siguientePaso()}
onAtras={() => flujo.pasoAnterior()}
/>
)}
</div>
)}
</Subscribe>
);
}
4. Configuración inicial de dependencias
const contenedorTelefono = new VerificarTelefonoContainer();
const contenedorContrasena = new ContrasenaContainer();
const flujoRegistro = new FlujoRegistroContainer();
flujoRegistro.inyectarPasos(contenedorTelefono, contenedorContrasena);
function App() {
return (
<Provider inject={[contenedorTelefono, contenedorContrasena, flujoRegistro]}>
<FormularioRegistro />
</Provider>
);
}
Manejo de errores y técnicas avanzadas
Para una experiencia robusta, el contenedor coordinador puede centralizar la gestión de errores:
class FlujoRegistroContainer extends Container {
state = { pasoActual: 1, error: null };
async siguientePaso() {
try {
this.setState({ error: null });
// Lógica de avance...
} catch (err) {
this.setState({ error: err.mensaje });
}
}
}
Para probar los contenedores de forma aislada, puedes inyectar deepndencias simuladas:
test('VerificarTelefonoContainer maneja respuesta exitosa', async () => {
const contenedor = new VerificarTelefonoContainer();
contenedor.setState({ telefono: '987654321', codigo: '000000' });
apiMock.verificarTelefono.mockResolvedValue({ exitoso: true });
const resultado = await contenedor.verificar();
expect(resultado).toBe(true);
expect(contenedor.state.verificado).toBe(true);
});
Buenas prácticas y recomendaciones
- Responsabilidad única: Cada contenedor debe manejar solo una etapa del flujo o una entidad de negocio.
- Inyección de dependencias: Usar
Provider injectpermite reemplazar contenedores fácilmente (ej. para pruebas). - Inmutabilidad: Siempre usar
setStatey no modificar directamente el estado. - Asincronía controlada: Emplear
async/awaitpara garantizar la secuencia correcta de actualizaciones de estado.
La combinación de Unstated y el patrón Cadena de Responsabilidad proporciona una estructura clara y mantenible para flujos de trabajo complejos. Este enfoque es particularmente efectivo en aplicaciones React donde se busca evitar la sobrecarga de herramientas de gestión de estado más grandes, manteniendo el código organizado y fácil de modificar. Para escenarios con mayor complejidad, la verrsión unstated-next ofrece una integración más directa con Hooks, abriendo nuevas posibilidades de implementación.