Implementación manual de Redux con React y Vite

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.

Etiquetas: Redux React Vite JavaScript gestión de estado

Publicado el 6-5 20:39