Escalando una aplicación Redux con patos

En previsión del inicio del curso, el "desarrollador de React.js" preparó una traducción de material útil.





¿Cómo se escala el front-end de su aplicación? ¿Cómo hacer que su código sea compatible seis meses después?

En 2015, Redux irrumpió en el mundo del desarrollo front-end y se estableció como un estándar más allá de React.

La compañía para la que trabajo recientemente terminó de refactorizar una gran base de código en React, donde implementamos redux en lugar de reflujo .

Tuvimos que dar este paso porque avanzar no era posible sin una aplicación bien estructurada y un conjunto claro de reglas.

La base del código ha existido por más de dos años y el reflujo ha estado presente desde el principio. Tuvimos que cambiar el código, fuertemente vinculado a los componentes React, que nadie había tocado durante más de un año.
Basado en la experiencia del trabajo realizado, creé este repositorio , que ayudará a explicar nuestro enfoque para organizar el código en redux.
Cuando aprende más sobre redux, acciones y reductores , comienza con ejemplos simples. Muchos tutoriales disponibles hoy no van más allá de ellos. Sin embargo, si está creando algo más complejo en Redux que una lista de tareas, necesitará una forma razonable de escalar su base de código con el tiempo.

Alguien dijo una vez que en informática no hay tarea más difícil que dar nombres a diferentes cosas. No pude estar en desacuerdo. En este caso, la estructuración de carpetas y la organización de archivos estarán en segundo lugar.

Veamos cómo solíamos abordar la organización del código.

Función vs característica


Existen dos enfoques generalmente aceptados para organizar las aplicaciones: función primero y función primero.
En la captura de pantalla de la izquierda, la estructura de la carpeta está organizada de acuerdo con el principio de la función primero y a la derecha, la función primero.



La función primero significa que sus directorios de nivel superior se nombran de acuerdo con los archivos que contiene. Entonces tienes: contenedores, componentes, acciones, reductores , etc.

Esto no escala en absoluto. A medida que su aplicación crezca y aparezca una nueva funcionalidad, agregará archivos a las mismas carpetas. Como resultado, tendrá que desplazarse por el contenido de una de las carpetas durante mucho tiempo para encontrar el archivo deseado.

Otro problema es combinar carpetas. Es probable que uno de los hilos de la aplicación requiera acceso a los archivos de todas las carpetas.

Una de las ventajas de este enfoque es que puede aislar, en nuestro caso, React from Redux. Por lo tanto, si desea cambiar la biblioteca de administración de estado, sabrá qué carpetas necesitará. Si necesita cambiar la biblioteca de vistas, puede dejar las carpetas con redux intacto.

La función principal significa que los directorios de nivel superior se nombrarán de acuerdo con la funcionalidad principal de la aplicación: producto, carrito, sesión.

Este enfoque se escala mucho mejor, ya que cada nueva característica se encuentra en una nueva carpeta. Sin embargo, no tiene una separación entre los componentes Redux y React. Cambiar uno de ellos a la larga no es una tarea fácil.

Además, tendrá archivos que no pertenecerán a ninguna función. Como resultado, todo se reduce a la carpeta común o compartida, porque también desea utilizar su código en diferentes funciones de su aplicación.

Combinando lo mejor de dos mundos


Aunque este no es el tema del artículo, quiero decir que los archivos de administración de estado de los archivos de la interfaz de usuario deben almacenarse por separado.

Piensa en tu aplicación a largo plazo. Imagine lo que le sucede a su código si cambia de Reaccionar a otra cosa. O piense en cómo su base de código usará ReactNative en paralelo con la versión web.

En el centro de nuestro enfoque está el principio de aislamiento. Reaccione el código en una carpeta llamada vistas y el código redux en otra carpeta llamada redux.

Esta separación en el nivel de entrada nos da la flexibilidad para organizar partes individuales de la aplicación de maneras completamente diferentes.

Dentro de la carpeta de vistasApoyamos la organización de los archivos de primera función. En el contexto de React, esto parece natural: páginas, diseños, componentes, mejoradores , etc.

Para no volverse loco con la cantidad de archivos en una carpeta, puede usar el enfoque de primera función dentro de estas carpetas.

Mientras tanto, en la carpeta redux ...

Introduciendo re-patos


Cada función de la aplicación debe corresponder a acciones y reductores separados, por lo que tiene sentido aplicar el enfoque de primera característica.

El enfoque modular originales patos hace que sea fácil trabajar con redux y ofrece una manera estructurada para agregar nueva funcionalidad a su aplicación.

Sin embargo, quería entender qué sucede cuando escala la aplicación. Nos dimos cuenta de que la forma de organizar un archivo por función satura la aplicación y hace que su soporte sea problemático.

Entonces aparecieron los re-patos . La solución fue dividir la funcionalidad en carpetas de pato.

duck/
├── actions.js
├── index.js
├── operations.js
├── reducers.js
├── selectors.js
├── tests.js
├── types.js
├── utils.js

La carpeta del pato debe:

  • Contiene toda la lógica de procesar solo UN concepto de su aplicación, por ejemplo: producto, carrito, sesión, etc.
  • Contiene el archivo index.js, que se exporta según las reglas de pato.
  • Almacene el código en un solo archivo que haga un trabajo similar, como reductores, selectores y acciones.
  • Contiene pruebas relacionadas con pato.

Por ejemplo, en este ejemplo, no utilizamos abstracciones construidas sobre redux. Al crear software, es importante comenzar con la menor cantidad de abstracción. Por lo tanto, verá que el valor de sus abstracciones no excede los beneficios de ellas.

Si quieres asegurarte de que la abstracción sea mala, mira este video de Cheng Lou .

Veamos el contenido de cada archivo.

Tipos


El archivo de tipos contiene los nombres de las acciones que ejecuta en su aplicación. Como buena práctica, debe tratar de cubrir el espacio de nombres correspondiente a la función a la que pertenecen. Este enfoque ayudará al depurar aplicaciones complejas.

const QUACK = "app/duck/QUACK";
const SWIM = "app/duck/SWIM";

export default {
    QUACK,
    SWIM
};

Comportamiento


Este archivo contiene todas las funciones del creador de la acción .

import types from "./types";

const quack = ( ) => ( {
    type: types.QUACK
} );

const swim = ( distance ) => ( {
    type: types.SWIM,
    payload: {
        distance
    }
} );

export default {
    swim,
    quack
};

Tenga en cuenta que todas las acciones están representadas por funciones, incluso si no están parametrizadas. Un enfoque coherente es la máxima prioridad para una gran base de código.

Operaciones


Para representar la cadena de operaciones ( Operaciones ), necesitará el middleware redux , para mejorar la función del despacho . Ejemplos populares son redux-thunk , redux-saga o redux-observable .

En nuestro caso, se usa redux-thunk . Necesitamos separar los thunks de los creadores de acciones, incluso a costa de escribir código adicional. Por lo tanto, definiremos la operación como un contenedor sobre las acciones .

Si la operación envía solo una acción , es decir, en realidad no usa redux-thunk , enviamos la función creadora de acción. Si la operación usa thunk , puede enviar muchas acciones y vincularlas mediante promesas .

import 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.js
const complexQuack = ( distance ) => ( dispatch ) => {
    dispatch( actions.quack( ) ).then( ( ) => {
        dispatch( actions.swim( distance ) );
        dispatch( /* any action */ );
    } );
}

export default {
    simpleQuack,
    complexQuack
};

Llámalos operaciones, thunks , sagas, epopeyas, como quieras. Simplemente identifique los principios de denominación y cúmplalos.

Al final, hablaremos sobre el índice y veremos que las operaciones son parte de la interfaz pública del pato. Las acciones están encapsuladas, las operaciones se vuelven accesibles desde el exterior.

Reductores


Si tiene una función más multifacética, definitivamente debe usar varios reductores para manejar estructuras de estado complejas. Además, no tengas miedo de usar tantos Combinadores Reductores como necesites. Esto permitirá trabajar con estructuras de objetos de estado más libremente.

import { combineReducers } from "redux";
import 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;

En una aplicación grande, el árbol de estado constará de al menos tres niveles. Las funciones reductoras deben ser lo más pequeñas posible y manejar solo construcciones de datos simples. La función combineReducers es todo lo que necesita para crear una estructura de estado flexible y mantenible.

Vea el ejemplo de proyecto completo y vea cómo usar combineReducers correctamente , especialmente en archivos reducers.jsy store.jsdonde estamos construyendo el árbol de estado.

Selectores


Junto con el selector de operaciones ( selector ) son parte de la interfaz pública duck. La diferencia entre operaciones y selectores es similar al patrón CQRS .

Las funciones del selector toman una porción del estado de la aplicación y devuelven algunos datos basados ​​en ella. Nunca realizan cambios en el estado de la aplicación.

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

export default {
    checkIfDuckIsInRange
};

Índice


Este archivo indica lo que se exportará desde la carpeta duck.
Es él:

  • Exporta la función reductora de pato por defecto.
  • Selectores de exportaciones y operaciones como exportaciones registradas.
  • Tipos de exportaciones si se requieren en otros patos.

import reducer from "./reducers";

export { default as duckSelectors } from "./selectors";
export { default as duckOperations } from "./operations";
export { default as duckTypes } from "./types";

export default reducer;

Pruebas


La ventaja de usar Redux con el marco de los patos es que puede escribir pruebas justo después del código que desea probar.

Probar su código en Redux es bastante sencillo:

import expect from "expect.js";
import reducer from "./reducers";
import 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 ) ;
        } );
    } );
} );

Dentro de este archivo puede escribir pruebas para reductores , operaciones, selectores, etc.
Podría escribir un artículo por separado sobre los beneficios de las pruebas de código, pero ya son suficientes, ¡así que solo pruebe su código!

Eso es todo


La buena noticia acerca de re-ducks es que puede usar la misma plantilla para todo su código redux.

El feature- enfoque de partición basada en su código redux ayuda a mantener tu aplicación flexible y escalable a medida que crece. Un enfoque de separación basado en funciones funcionará bien al construir componentes pequeños que son comunes a diferentes partes de la aplicación.

Puede echar un vistazo a la base de código de react-redux-example completa aquí . Tenga en cuenta que el repositorio está trabajando activamente.

¿Cómo organizas tus aplicaciones redux? Espero recibir comentarios sobre el enfoque descrito.

Nos vemos en el curso .

All Articles