ReactJs/Redux: Una estructura para mejorar la escalabilidad

Fabian Zamudio
Al iniciar en el mundo de React una forma sencilla y rápida es basar nuestros primeros proyectos en create-react-app, esta es una excelente herramienta pero hay que tener en cuenta que esta estructura solo es viable para proyectos medianos o chicos. Cuando nos encontramos con un sistema que potencialmente requiera una complejidad considerable o el mismo requiera escalar (en cuanto a tamaño del código se refiere) necesitamos estructurarlo de una forma más inteligente, por lo que en este post mi intención es explicar una estructura que en mi experiencia me ha dado resultado como a tantos otros desarrolladores.

 

Cuack, cuack, cuack…


Para lograr nuestro objetivo utilizaremos el patrón DUCK que propone separar nuestro código con el concepto de feature-first, es decir no usar la estructura de containers, components, actions, reducers, etc. (que es propuesta en la documentación de Redux) ya que no permiten una buena escalabilidad. Esto es debido al simple hecho que al crecer la aplicación (cantidad de componentes, reducers, etc) se mal gasta tiempo en recorrer carpetas y archivos hasta encontrar el correcto. Aunque esto parezca un detalle menor, solo hay que pensar un segundo cuanto tiempo nos lleva diariamente ese simple scroll y cuantos archivos abiertos tenemos para desarrollar una funcionalidad.

En el caso de feature-first se pretende agrupar los archivos relacionados en una misma carpeta como podrían ser session, users, product, etc , la idea se basa en la premisa que en la mayoría de nuestro tiempo desarrollando, usamos los archivos que están relacionados. Un ejemplo que sale a simple vista podría ser cuando se esta trabajando en el componente de producto, donde la probabilidad de trabajar en sus reducers o stores es muy alta. Esto también nos permite en algunos casos poder mejorar la reutilización de componentes entre aplicaciones ya que con pocos cambios (generalmente con lo que a Redux se refiere) es viable el uso en otro contexto.

 

Cada cosa en su lugar


duck/
├── actions.js
├── index.js
├── operations.js
├── reducers.js
├── selectors.js
├── tests.js
├── types.js
├── utils.js
 
Con los conceptos anteriores logramos dar un gran salto pero todavía falta mucho más, lo próximo que atenderemos es la posibilidad de definir ciertos archivos que generalmente se encuentran en todos los componentes. En general encontré útil la creación de los siguientes archivos:



types.js

Uno de los errores más comunes que encuentro en el código (y no solo en ReactJs) es el uso de comparaciones con string. Esto provoca que ante el cambio del string debamos cambiar este en todos los archivos donde se usó, por lo cual para evitar esto debemos definirlas como constantes en un solo archivo. La idea de tener types.js es justamente tener una colección de constantes usadas en el componente como puede ser el caso de los actions y reducers.

const QUACK = "app/duck/QUACK";const SWIM = "app/duck/SWIM"; export {    QUACK,    SWIM};


 
actions.js

Toda action de Redux debe estar definida usando alguna opción del archivo types.js y siempre se deben exportar funciones.

import * as types from "./types"; const quack = ( ) => ( {    type: types.QUACK} ); const swim = ( distance ) => ( {    type: types.SWIM,    payload: {        distance    }} ); export {    swim,    quack};
 

 
operations.js

Acá se define cualquier lógica que corresponda a un dispatch de una acción actuando como un middleware para hacer llamadas asincrónicas, por ejemplo.

import * as actions from "./actions"; // This is a link to an action defined in actions.js.const simpleQuack = actions.quack; // This is a thunk which dispatches multiple actions from actions.jsconst complexQuack = ( distance ) => ( dispatch ) => {    dispatch( actions.quack( ) ).then( ( ) => {        dispatch( actions.swim( distance ) );        dispatch( /* any action */ );    } );} export {    simpleQuack,    complexQuack};
 

 
reducers.js

Para el manejo de estados pondremos nuestros reducers en este archivo, definiendo uno por cada action usando un switch, lo que resulta en uno mucho menor que tener un solo reducer para todas las acciones de nuestra aplicación. Luego en la aplicación se utilizara combineReducers() y createReducer() para exportarlo. 

import { combineReducers } from "redux";import * as types from "./types"; /* State Shape{    quacking: bool,    distance: number}*/ const quackReducer = ( state = false, action ) => {    switch( action.type ) {        case types.QUACK: return true;        /* ... */        default: return state;    }} const distanceReducer = ( state = 0, action ) => {    switch( action.type ) {        case types.SWIM: return state + action.payload.distance;        /* ... */        default: return state;    }} const reducer = combineReducers( {    quacking: quackReducer,    distance: distanceReducer} ); export default reducer;
 


selectors.js

Este es un archivo de los opcionales pero que nos ayuda a tener un mejor control de nuestro código. Hay que tener en cuenta que la idea principal es que las funciones definidas puedan ser usadas por fuera de nuestra carpeta duck.

function checkIfDuckIsInRange( duck ) {    return duck.distance > 1000;} export {    checkIfDuckIsInRange};

 

index.js

Aquí tendremos la interfaz de nuestro componente duck exportando reducers, selectors y operaciones, types, etc.

import reducer from "./reducers"; import * as duckSelectors from "./selectors";import * as duckOperations from "./operations";import * as duckTypes from "./types"; export {    duckSelectors,    duckOperations,    duckTypes}; export default reducer;
 
 

test.js

Siempre es necesario construir algún tipo de pruebas para correr y probar nuestra aplicación, en este caso aportaremos las pruebas de nuestro componente. 

import expect from "expect.js";import reducer from "./reducers";import * as actions from "./actions"; describe( "duck reducer", function( ) {    describe( "quack", function( ) {        const quack = actions.quack( );        const initialState = false;         const result = reducer( initialState, quack );         it( "should quack", function( ) {            expect( result ).to.be( true ) ;        } );    } );} );



Agilizando las cosas


Ahora que vimos toda la estructura duck, tal vez alguien puede pensar ¿Pierdo mucho tiempo creando carpetas, archivos, etc?. Y no es erróneo su pregunta, algunos otros han pensado lo mismo y por suerte han creado herramientas que nos ayudan a afrontar todo el proceso de una forma más sencilla.

Actualmente uso una combinación la cual permite generar la estructura duck con una simple linea de comando, en principio hago uso de BattleCry a lo que agrego la extensión de Erik Rasmussen que nos provee de un Scaffolding para tal fin.

npm install -g battlecry
cry download generator erikras/ducks-modular-redux
cry init duck

 La instalación de ambos es sencilla y nos provee una herramienta muy poderosa. Ahora solo queda poner en práctica lo aprendido, evaluar la propuesta y dejar tu opinión sobre la misma.

Have a nice coding!!


WRITTEN BY

Software Developer. Follow me: LinkedIn @fabian-zamudio-dev | Github @zamudio-fabian | Twitter @fabian_zamudio