Inicio
Blog
Cómo lograr un contexto escalable, mantenible y testeable en ReactJS (Parte 1)
Tutoriales

Cómo lograr un contexto escalable, mantenible y testeable en ReactJS (Parte 1)

Desde hace algún tiempo, se ha popularizado el uso de Context para solucionar el problema de compartir estados globales dentro de una jerarquía compleja de componentes en una página o parte de esta, y evitar que en ese proceso se propaguen propiedades de padres a hijos (prop-drilling).

Aunque no es recomendable el uso de Context para situaciones complejas que involucran múltiples estados, ya que puede implicar múltiples renderizados en los suscriptores hijos (ver Sebastian Markbage, del equipo de desarrollo de React), algunas veces puede ser más productivo y ligero frente a otras soluciones, como Redux. 

El mayor de los problemas llega cuando nuestra funcionalidad comienza a crecer y, con ella, nuestro contexto. Para cubrir los nuevos requerimientos, comenzamos a agregar más estados y lógica dentro de este, y al final tenemos un archivo con demasiadas líneas de código, difícil de leer y mantener si no diseñamos nuestro contexto para soportar este crecimiento.

En este artículo les comparto una propuesta de diseño para mantener los contextos escalables, mantenibles y testeables. Mediante un ejemplo práctico mostraremos cómo lograr implementarlo, suponiendo que está familiarizado con su uso.

Supongamos que queremos implementar una vista con un listado de clientes, que nos permitirá seleccionar un cliente y mostrar su información en otro componente. Algo así:
ReactJS (Contexto )Podemos identificar al menos tres componentes que, además, requieren conocer el cliente seleccionado: CustomerList, CustomerCard y PageHeader. Para compartir el cliente seleccionado como un estado global, creamos un contexto (nótese que puede haber otras soluciones, pero usaremos un contexto para cumplir el objetivo del artículo). Comencemos.

Declaración del contexto, estructura y organización

Generalmente, dentro de nuestro proyecto tendremos una carpeta en la que implementamos nuestros contextos. Es una buena práctica, además, tener el contexto lo más cerca posible de las entidades de negocio donde se aplicará. Podríamos tener una estructura de carpetas como esta:

src/
  Customer/
       contexts/
            CustomerContext/
                       index.js

Como se aprecia, nuestro contexto CustomerContext es una carpeta, no un archivo (CustomerContext.js). Aquí es donde comienza el diseño propuesto. La declaración de nuestro contexto estará en el archivo index.js (enseguida explicaremos por qué). En este archivo declararemos el Context con su Provider y estados:

// CustomerContext/index.js
import React, { createContext, useContext, useReducer } from 'react';
import {  reducer } from './reducer';

const initialState = {
  selectedCustomer: null
};

export const CustomerContext = createContext({
  state: initialState,
  dispatch: () => null,
});

export const CustomerProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <CustomerContext.Provider
      value={{
        state,
        dispatch,
      }}
    >
      {children}
    </CustomerContext.Provider>
  );
};

export const useCustomerContext = () => {
  const context = useContext(CustomerContext);
  if (!context) {
    throw new Error('CustomerContext must be used within a CustomerContextProvider');
  }
  return context;
};

En index.js solo tendremos eso, la creación del Context (CustomerContext) y la declaración de sus estados (SelectedCustomer). El único motivo por el cual cambiaría este archivo, sería que agregáramos un nuevo estado. 

Además, no se debe incluir lógica en este archivo. 

Seguramente adivinó nuestra próxima propuesta: el empleo de useReducer, no del tradicional useState, para la gestión de estados.

También te puede interesar :  Observatorio Cubano de Ciencias Económicas, desarrollado con Reactjs

Empleando useReducer para el manejo del estado

No es objetivo de este tutorial describir las ventajas de useReducer sobre useState en la gestión de estados complejos. Puede encontrar varios artículos sobre el tema. Le recomendamos los siguientes:

Sin embargo, sí hablaremos de una ventaja de la función reductora: puede estar desacoplada. Como función al fin, podemos crearla en un archivo e importarla en nuestro contexto, como hacemos en la línea #2. Luego, la usamos en la #14 del código anterior. 

Además, puede ser probada fácilmente mediante pruebas unitarias, como probaríamos cualquier función.

Entonces, nuestro contexto se vería así:

CustomerContext/ 
    index.js          
    reducer.js

La implementación de la función reductora es la siguiente:

// CustomerContext/reducer.js
export const reducer = (state, {type, payload}) => {
  switch (type) {
    case 'SELECT_CUSTOMER':
      return {
        ...state,
        selectedCustomer: payload
      };
    case 'DESELECT_CUSTOMER':
      return {
        ...state,
        selectedCustomer: null
      };
    default:
      return state;
  }
};

Cada acción modifica el estado, y una misma acción podría modificar varias variables del estado (otra de las ventajas de useReducer). El escalado del reducer sería causado por nuevas acciones introducidas.

Actualización del estado, dispatchers

Ya tenemos nuestro contexto, el estado y nuestra función reductora, que genera los estados por una acción. Ahora debemos crear el mecanismo para actualizar el estado del contexto; es decir, nuestra variable SelectedCustomer.

En la implementación de nuestro contexto (index.js), exportamos el estado (línea #18) y creamos el hook useCustomerContext para que los suscriptores puedan consumirlo de esta forma:

// App.js
import './App.css';
import { CustomerProvider } from "./Cusmoter/contexts/CustomerContext";
import CustomerList from "./Cusmoter/containers/CustomerList";

function App() {
  return (
    <CustomerProvider>
      <Page className="App">
        <PageHeader />
        <CustomerList />
        <CustomerCard />
      </Page>
    </CustomerProvider>
  );
}

export default App;

Componente CustomerList consumiendo el estado del Context:

import React from 'react';
import { useCustomerContext } from "../../contexts/CustomerContext";

const CustomerList = () => {
  const {state: { selectedCustomer }} = useCustomerContext();

  return <List selected={selectedCustomer} />;
}

export default CustomerList;

Además de exportar el estado, el hook del contexto (useCustomerContext) emplea el método dispatch (vea useReducer), encargado de enviar las acciones y los parámetros al reductor para actualizar el estado.

Al exportarlo, podemos crear nuestras funciones modificadoras o dispatchers como funciones desacopladas. En este caso, serían hooks para poder usar el hook del contexto y, además, usarlo en nuestros componentes hijos.

Creemos el hook que que permite actualizar el cliente seleccionado (selectedCustomer), y llamémosle useSelectCustomer.

Podemos usar la siguiente estructura:

CustomerContext/         
    dispatchers/              
        useSelectCustomer.js         
    index.js         
    reducer.js
// CustomerContext/dispatchers/useSelectCustomer.js 
import { useCustomerContext } from "../index";

export const useSelectCustomer = () => {
  const { dispatch } = useCustomerContext();

  return (customer) => {
    dispatch({
      type: 'SELECT_CUSTOMER',
      payload: customer,
    });
  };
};

Nótese cómo utilizamos el método dispatch de nuestro contexto para que el hook nos devuelva una función que permite actualizar nuestro estado al enviar la acción ‘SELECT_CUSTOMER’ con su payload. En nuestros componentes suscritos podemos usarlo así:

import React from 'react';
import { useCustomerContext } from "../../contexts/CustomerContext";
import {useSelectCustomer} from "../../contexts/CustomerContext/dispatchers/useSelectCustomer";

const CustomerList = () => {
  const {state: { selectedCustomer }} = useCustomerContext();
  const selectCustomer = useSelectCustomer();

  return <List onSelect={selectCustomer} selected={selectedCustomer} />;
}

export default CustomerList;

Ya tenemos nuestra estructura lista y desacoplada en varios archivos, cada uno con su función muy clara. Nuestro contexto puede ahora soportar los nuevos requerimientos sin convertirse en un dolor de cabeza inmantenible.

Los nuevos estados los colocamos en CustomerContext/index.js. La lógica que actualiza el estado la creamos en hooks independientes dentro de CustomerContext/dispatchers/, al igual que sus correspondientes acciones en el archivo CustomerContext/reducer.js

Si deseamos un nuevo requerimiento para, por ejemplo, eliminar el cliente seleccionado, filtrar u ordenar el listado de clientes, ya no es problema crear nuevos hooks, estados y acciones con ese fin, tengan la complejidad que tengan:

CustomerContext/ 
    dispatchers/         
       useSelectCustomer.js         
       useRemoveCustomer.js         
       useFilterCustomers.js         
       useSortCustomers.js 
    index.js     
    reducer.js

Conclusiones

Logramos un diseño para nuestro contexto que puede crecer con nuevos requerimientos, sin tener un contexto ilegible saturado de líneas de código. Además, cada porción de código es fácilmente localizable, y podemos probar cada método sin dificultad alguna. 

En la segunda parte del artículo veremos cómo quedaría nuestra implementación usando TypeScript, además veremos ejemplos de cómo podemos diseñar nuestras pruebas unitarias para probar cada parte del contexto.

¿De cuánta utilidad te ha parecido este contenido?

¡Haz clic en una estrella para puntuar!

Promedio de puntuación 4.3 / 5. Recuento de votos: 6

Hasta ahora, ¡no hay votos!. Sé el primero en puntuar este contenido.

¡Siento que este contenido no te haya sido útil!

¡Déjame mejorar este contenido!

Dime, ¿cómo puedo mejorar este contenido?

Comentarios

Alberto B.Mederos Rodríguez 13 de octubre de 2021. 01:44

Mis respetos, a nivel de algoritmo lo veo y lo entiendo, pero cero código, así que hermano mío, este baile llegó tarde a mi. No obstante los sigo para disfrutar de esas maravillosas implementaciones que te tiras…

Responder

Déjanos tu comentario

Daisy Diana Vargas Vento

“De Dofleini Software hemos aprendido la competitividad que exige este mercado”

Daisy Diana Vargas Vento Subdirectora de la División Integradora de Negocios de Desoft