Este artículo explora la implementación de un estado global administrado, similar a Redux, desde cero usando React y Vite. El objetivo es comprender los principios fundamentales detrás de las bibliotecas de gestión de estado.
Estructura del Proyecto y Configuración Inicial
Se utiliza Vite como herramienta de construcción para un desarrollo rápido. La estructura de archivos del proyecto es la siguiente:
│ App.jsx
│ index.jsx
│ miRedux.js
│ style.css
│
└─conectores
conectarAUsuario.js
El archivo package.json define las dependencias esenciales de React y los scripts de desarrollo y compilación de Vite.
Componentes de la Aplicación
El componente principal (App.jsx) establece el estado inicial y el reductor, y provee el store a los componentes hijos mediante un Provider personalizado.
import React from 'react'
import { Proveedor, crearStore, conectar } from './miRedux.js'
import { conectarAUsuario } from './conectores/conectarAUsuario'
// Definición del reductor
const reductor = (estadoActual, accion) => {
if (accion.tipo === 'actualizarUsuario') {
return {
...estadoActual,
usuario: {
...estadoActual.usuario,
...accion.datos
}
}
}
return estadoActual
}
// Estado inicial de la aplicación
const estadoInicial = {
usuario: { nombre: 'frank', edad: 18 },
grupo: { nombre: 'Equipo Frontend' }
}
// Instancia del store creada con nuestro módulo
const store = crearStore(reductor, estadoInicial)
export const App = () => (
<Proveedor store={store}>
<HijoMayor />
<HijoMediano />
<HijoMenor />
</Proveedor>
)
const HijoMayor = () => {
console.log('HijoMayor se ejecutó ' + Math.random())
return <section>Hijo Mayor<MostrarUsuario /></section>
}
const HijoMediano = () => {
console.log('HijoMediano se ejecutó ' + Math.random())
return <section>Hijo Mediano<ModificadorUsuario /></section>
}
const HijoMenor = conectar(estado => ({
grupo: estado.grupo
}))(({ grupo }) => {
console.log('HijoMenor se ejecutó ' + Math.random())
return (
<section>Hijo Menor
<div>Grupo: {grupo.nombre}</div>
</section>
)
})
const MostrarUsuario = conectarAUsuario(({ usuario }) => {
console.log('MostrarUsuario se ejecutó ' + Math.random())
return <div>Usuario: {usuario.nombre}</div>
})
const llamadaApi = () => new Promise((res) => {
setTimeout(() => res({ datos: { nombre: 'frank (tras 3s)' } }), 3000)
})
const ModificadorUsuario = conectar(null, null)(({ estado, dispatch }) => {
console.log('ModificadorUsuario se ejecutó ' + Math.random())
const manejarClic = () => {
dispatch({ tipo: 'actualizarUsuario', datos: llamadaApi().then(r => r.datos) })
}
return (
<div>
<div>Usuario: {estado.usuario.nombre}</div>
<button onClick={manejarClic}>Obtener usuario (async)</button>
</div>
)
})
Implementación del Módulo de Estado (miRedux.js)
Este archivo contiene la implementación completa del patrón de flujo de datos unidireccional.
import React, { useEffect, useState } from 'react'
let estadoGlobal = undefined
let reductorGlobal = undefined
let subscriptores = []
const actualizarEstado = (nuevoEstado) => {
estadoGlobal = nuevoEstado
subscriptores.forEach(fn => fn(estadoGlobal))
}
const storeInterno = {
obtenerEstado() {
return estadoGlobal
},
despachar(accion) {
actualizarEstado(reductorGlobal(estadoGlobal, accion))
},
suscribir(fn) {
subscriptores.push(fn)
return () => {
const idx = subscriptores.indexOf(fn)
subscriptores.splice(idx, 1)
}
}
}
let despachoOriginal = storeInterno.despachar
// Middleware para funciones
let despachoConFunciones = (accion) => {
if (typeof accion === 'function') {
accion(despachoConFunciones)
} else {
despachoOriginal(accion)
}
}
// Middleware para promesas
let despachoFinal = (accion) => {
if (accion.datos instanceof Promise) {
accion.datos.then(datosResueltos => {
despachoFinal({ ...accion, datos: datosResueltos })
})
} else {
despachoConFunciones(accion)
}
}
export const crearStore = (reductor, estadoInicial) => {
estadoGlobal = estadoInicial
reductorGlobal = reductor
return { ...storeInterno, despachar: despachoFinal }
}
const hanCambiado = (antiguo, nuevo) => {
for (const clave in antiguo) {
if (antiguo[clave] !== nuevo[clave]) return true
}
return false
}
export const conectar = (selectorLectura, selectorEscritura) => (Componente) => {
const ComponenteEnvuelto = (props) => {
const datos = selectorLectura ? selectorLectura(estadoGlobal) : { estado: estadoGlobal }
const acciones = selectorEscritura ? selectorEscritura(despachoFinal) : { despacho: despachoFinal }
const [, forzarActualizacion] = useState({})
useEffect(() => {
const desuscribir = storeInterno.suscribir(() => {
const nuevosDatos = selectorLectura ? selectorLectura(estadoGlobal) : { estado: estadoGlobal }
if (hanCambiado(datos, nuevosDatos)) {
forzarActualizacion({})
}
})
return desuscribir
}, [selectorLectura])
return <Componente {...props} {...datos} {...acciones} />
}
return ComponenteEnvuelto
}
const ContextoApp = React.createContext(null)
export const Proveedor = ({ store, hijos }) => (
<ContextoApp.Provider value={store}>
{hijos}
</ContextoApp.Provider>
)
Conector Específico para el Usuario
El archivo conectarAUsuario.js reutiliza la función conectar para crear un conector especializado, promvoiendo la reutilización de código.
import { conectar } from '../miRedux'
const selectorUsuario = (estado) => ({ usuario: estado.usuario })
const accionesUsuario = (despacho) => ({
actualizarUsuario: (atributos) => despacho({ tipo: 'actualizarUsuario', datos: atributos })
})
export const conectarAUsuario = conectar(selectorUsuario, accionesUsuario)
Esta implementación cubre los conceptos centrales: creación del store, suscripción a cambios, despacho de acciones (incluyendo asincronía), y la integración con React mediante componentes de orden superior y Context.