Recientemente he estado buscando una estructura de archivos para los proyectos con ReactJS que sea lo suficientemente flexible en crecimiento, semanticamente relevante y que albergue los distintos procesos y buenas prácticas.
Encontré varias configuraciones las cuales contemplaban mayor o menor cantidad de features, discrepaban en el criterio de segmentación, si era por tipo de archivo o por funcionalidad, la arquitectura de servicios que utilizaban, en fin, todo se reduce a cuantas variables se tienen y como agruparlas, así que empezaré definiendo las primeras.
Voy a empezar con la estructura básica de una aplicación tomando como base create-react-app. Un proyecto básico, un punto de entrada + un componente + yarn build y listo, el proyecto es funcional y una auténtica posibilidad.
my-app ├── node_modules ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ └── serviceWorker.js ├── .gitignore ├── package.json └── README.md
La primera feature a agregar será el bundler (en este caso usaré webpack) para tener acceso a las variables de configuración generales del ambiente de desarrollo.
Posiblemente, lo más natural sería ir agregango más y más componentes, tanto en número como en jerarquía, ir deconstruyendo el problema y modelar su estructura en piezas más pequeñas y relacionadas.
my-app ├── build ├── node_modules ├── public ├── src │ ├── component-1.js │ ├── component-1.1.js │ ├── component-1.2.js │ ├── component-1.2.1.js │ ├── component-1.2.2.js │ ├── component-2.js │ ├── component-3.js │ ├── component-3.1.js │ ├── component-3.2.js │ ├── component-4.js │ ├── component-5.js │ ├── component-5.1.js │ ├── component-5.2.js │ └── index.js ├── package.json ├── webpack.config.js └── README.md
Ese mismo crecimiento es insostenible en una sola página y los componentes generalmente no se nombran de acuerdo a su jerarquía si no en base a su función, así que el nombre component.1.2.1 podría ser más parecido a items-recientes, el punto con esto es que el ordenamiento de los archivos generalmente es alfabético y la visualización organizada que presento en el diagrama anterior es puramente ilustrativa. Tomando en cuenta estas variables y re-estructurando los contenedores podría parecerse a algo así
my-app ├── build ├── node_modules ├── public ├── src │ ├── components │ │ ├── page-1 │ │ │ ├── sub-component-name-1 │ │ │ │ ├── sub-component-name-1-container.js │ │ │ │ └── sub-component-name-1-view.js │ │ │ ├── page-1-container.js │ │ │ └── page-1-view.js │ │ ├── page-2 │ │ │ ├── page-2-container.js │ │ │ └── page-2-view.js │ └── index.js ├── package.json ├── webpack.config.js └── README.md
Para mejorar la legibilidad en los imports y apoyar la encapsulación de un componente, una práctica que se utiliza es la de agregar un index.js a cada componente incluyendo la lista de exports, tanto el default como cualquier otro componente que esté disponible publicamente, imitando la funcionalidad de un package, algo que agradecerá luego otra persona que necesite ver el código o bien uno mismo en un futuro, ya que los componentes públicos y sus relaciones estarán en un único archivo y no habrá necesidad de revisar archivo por archivo para conocerlo
// page-1 > index.js
imports Page1 from './page-1-container';
imports Page1SubComponent from './sub-component-name-1';
export default Page1;
export Page1SubComponent;
// importando los componentes desde otro componente
imports Page1, { Page1SubComponent } from 'components/page-1';
// lo cual es mucho más claro y legible que
imports Page1 from 'components/page-1/page-1-container';
imports Page1SubComponent from 'components/page-1/sub-component-name-1/sub-component-name-1-container';
// un beneficio evidente en la legibilidad
Agregarle funcionalidad a la página y convertirla en una aplicación, agregar un router, separar las rutas, agregar el dinamic loading por ruta
my-app ├── build ├── node_modules ├── public ├── src │ ├── components │ │ ├── app │ │ │ └── app.js │ │ ├── page-1 │ │ │ ├── sub-component-name-1 │ │ │ │ ├── sub-component-name-1-container.js │ │ │ │ └── sub-component-name-1-view.js │ │ │ ├── page-1-container.js │ │ │ └── page-1-view.js │ │ ├── page-2 │ │ │ ├── page-2-container.js │ │ │ └── page-2-view.js │ ├── routes │ │ ├── home │ │ │ ├── page-1.js │ │ │ └── index.js │ │ └── route-2 │ │ ├── page-2.js │ │ └── index.js │ └── index.js ├── package.json ├── webpack.config.js └── README.md
Cambiar la estructura de agrupación de páginas a módulos para enfocarse en el ámbito más que en la estructura.
Reutilización, un folder core con un framework de componentes, otro commons con componentes compartidos entre la app, p.e. header, footer…
my-app ├── build ├── node_modules ├── public ├── src │ ├── components │ │ ├── app │ │ │ ├── app.js │ │ ├── core │ │ ├── commons │ │ ├── module-1 │ │ │ ├── component-name-1 │ │ │ │ ├── sub-component-name-1 │ │ │ │ │ ├── sub-component-name-container-1.js │ │ │ │ │ └── sub-component-name-view-1.js │ │ │ │ ├── component-name-container.js │ │ │ │ ├── component-name-view.js │ │ │ │ └── index.js │ │ │ ├── component-name-2 │ │ │ │ ├── component-name-container.js │ │ │ │ ├── component-name-container.test.js │ │ │ │ ├── component-name-view.js │ │ │ │ ├── component-name-view.test.js │ │ │ │ └── index.js │ │ ├── pages │ │ │ ├── page-1.js │ │ │ └── page-2.js │ ├── routes │ │ ├── home │ │ │ ├── home.js │ │ │ └── index.js │ │ └── route-1 │ │ ├── route-1.js │ │ └── index.js │ └── index.js ├── package.json ├── webpack.config.js └── README.md
Estilos, ahora el color y presentación. Aquí hay bastantes opciones de como hacerlo pero lo simplificaré a SASS y styled-components. El respectivo folder de resources con los assets necesarios. Para más información respecto al tema se puede encontrar buscando CSS in JS
my-app ├── build ├── node_modules ├── public ├── src │ ├── components │ │ ├── app │ │ │ ├── app.scss │ │ │ └── app.js │ │ ├── core │ │ ├── commons │ │ ├── module-1 │ │ │ ├── component-name-1 │ │ │ │ ├── sub-component-name-1 │ │ │ │ │ ├── sub-component-name-container-1.js │ │ │ │ │ └── sub-component-name-view-1.js │ │ │ │ ├── component-name-container.js │ │ │ │ ├── component-name-view.js │ │ │ │ └── index.js │ │ │ ├── component-name-2 │ │ │ │ ├── component-name-container.js │ │ │ │ ├── component-name-view.js │ │ │ │ └── index.js │ │ ├── pages │ │ │ ├── page-1.js │ │ │ └── page-2.js │ ├── routes │ ├── resources │ │ ├── assets │ │ │ ├── images │ │ │ │ └── logo.svg │ │ │ ├── fonts │ │ │ └── scss │ ├── index.js │ └── index.scss ├── package.json ├── webpack.config.js └── README.md
El testing, unit-testing y algunas pruebas de integración inherentes a la jerarquía
my-app ├── build ├── node_modules ├── public ├── src │ ├── components │ │ ├── app │ │ │ ├── app.scss │ │ │ ├── app.js │ │ │ └── app.spec.js │ │ ├── core │ │ ├── commons │ │ ├── module-1 │ │ │ ├── component-name-1 │ │ │ │ ├── sub-component-name-1 │ │ │ │ │ ├── sub-component-name-container-1.js │ │ │ │ │ ├── sub-component-name-container-1.spec.js │ │ │ │ │ ├── sub-component-name-view-1.js │ │ │ │ │ └── sub-component-name-view-1.spec.js │ │ │ │ ├── component-name-container.js │ │ │ │ ├── component-name-container.spec.js │ │ │ │ ├── component-name-view.js │ │ │ │ ├── component-name-view.spec.js │ │ │ │ └── index.js │ │ │ ├── component-name-2 │ │ │ │ ├── component-name-container.js │ │ │ │ ├── component-name-container.spec.js │ │ │ │ ├── component-name-view.js │ │ │ │ ├── component-name-view.spec.js │ │ │ │ └── index.js │ │ ├── pages │ │ │ ├── page-1.js │ │ │ ├── page-1.spec.js │ │ │ ├── page-2.js │ │ │ └── page-2.spec.js │ ├── routes │ ├── resources │ │ ├── assets │ │ │ ├── images │ │ │ ├── fonts │ │ │ └── scss │ ├── index.js │ └── index.scss ├── package.json ├── webpack.config.js └── README.md
Si bien a veces es más rápido hacer un componente y e integrarlo directamente con todo el sitio, desarrollarlo aisladamente permite una mejor atención al detalle y menos distracción, por no hablar de la velocidad al desarrollarlo y probarlo (solo cargar el componente en cuestión y no todo el sitio). Por esto y mucho más, el folder de stories y el respectivo folder de mock-data para herramientas como storybook
my-app ├── build ├── node_modules ├── public ├── src │ ├── components │ │ ├── app │ │ │ ├── app.scss │ │ │ ├── app.js │ │ │ └── app.spec.js │ │ ├── core │ │ ├── commons │ │ ├── module-1 │ │ │ ├── component-name-1 │ │ │ │ ├── sub-component-name-1 │ │ │ │ │ ├── sub-component-name-container-1.js │ │ │ │ │ ├── sub-component-name-container-1.spec.js │ │ │ │ │ ├── sub-component-name-view-1.js │ │ │ │ │ └── sub-component-name-view-1.spec.js │ │ │ │ ├── component-name-container.js │ │ │ │ ├── component-name-container.spec.js │ │ │ │ ├── component-name-view.js │ │ │ │ ├── component-name-view.spec.js │ │ │ │ └── index.js │ │ │ ├── component-name-2 │ │ │ │ ├── component-name-container.js │ │ │ │ ├── component-name-container.spec.js │ │ │ │ ├── component-name-view.js │ │ │ │ ├── component-name-view.spec.js │ │ │ │ └── index.js │ │ ├── pages │ │ │ ├── page-1.js │ │ │ ├── page-1.spec.js │ │ │ ├── page-2.js │ │ │ └── page-2.spec.js │ ├── routes │ ├── resources │ │ ├── assets │ │ │ ├── images │ │ │ ├── fonts │ │ │ └── scss │ │ ├── documentation │ │ ├── mock-data │ │ └── stories │ ├── index.js │ └── index.scss ├── package.json ├── webpack.config.js └── README.md
El manejo del estado y el contexto de la aplicación con redux, mobx o algo más simple como baobab. Esto también es otro tema bastante discutido, el nivel de granularidad del estado, si por módulo o por componente, verlo global o modular, en dónde debieran de estar los archivos, para el caso de redux si debieran o no separarse por tipo de archivo o por ámbito.
Otras utilierías, la obtención de datos, los hooks, etc…
my-app ├── build ├── node_modules ├── public ├── src │ ├── components │ ├── routes │ ├── resources │ │ ├── assets │ │ ├── documentation │ │ ├── mock-data │ │ └── stories │ ├── state │ │ ├── pages │ │ ├── entities │ │ └── store.js │ ├── utils │ │ ├── hooks │ │ └── helpers │ ├── index.js │ ├── index.scss ├── package.json ├── webpack.config.js └── README.md
Múltiples lenguajes, el i18n y las traducciones. Además del folder de configuración general de la aplicación con las constantes globales
my-app ├── build ├── node_modules ├── public ├── src │ ├── config │ ├── components │ ├── routes │ ├── resources │ │ ├── assets │ │ ├── i18n │ │ ├── documentation │ │ ├── mock-data │ │ └── stories │ ├── state │ ├── utils │ ├── index.js │ ├── index.scss ├── package.json ├── webpack.config.js └── README.md
Building y deploy. El service worker y los polyfills, las configuraciones de linting, GIT y su .gitignore
my-app ├── build ├── node_modules ├── public ├── src │ ├── config │ ├── components │ ├── routes │ ├── resources │ │ ├── assets │ │ ├── i18n │ │ ├── documentation │ │ ├── mock-data │ │ └── stories │ ├── state │ ├── utils │ ├── index.js │ ├── index.scss │ ├── polyfill.js │ └── service-worker.js ├── .gitignore ├── .eslintrc ├── .eslintignore ├── .prettierrc ├── package.json ├── webpack.config.js └── README.md
Finalmente como resultado
my-app ├── build ├── node_modules ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── config │ ├── components │ │ ├── app │ │ │ ├── app.scss │ │ │ ├── app.js │ │ │ └── app.spec.js │ │ ├── core │ │ ├── commons │ │ ├── module-1 │ │ │ ├── component-name-1 │ │ │ │ ├── sub-component-name-1 │ │ │ │ │ ├── sub-component-name-container-1.js │ │ │ │ │ ├── sub-component-name-container-1.spec.js │ │ │ │ │ ├── sub-component-name-view-1.js │ │ │ │ │ └── sub-component-name-view-1.spec.js │ │ │ │ ├── component-name-container.js │ │ │ │ ├── component-name-container.spec.js │ │ │ │ ├── component-name-view.js │ │ │ │ ├── component-name-view.spec.js │ │ │ │ └── index.js │ │ │ ├── component-name-2 │ │ │ │ ├── component-name-container.js │ │ │ │ ├── component-name-container.spec.js │ │ │ │ ├── component-name-view.js │ │ │ │ ├── component-name-view.spec.js │ │ │ │ └── index.js │ │ ├── pages │ │ │ ├── page-1.js │ │ │ ├── page-1.spec.js │ │ │ ├── page-2.js │ │ │ └── page-2.spec.js │ ├── routes │ │ ├── home │ │ │ ├── home.js │ │ │ └── index.js │ │ └── route1 │ │ ├── route1.js │ │ └── index.js │ ├── utils │ │ ├── hooks │ │ └── helpers │ ├── resources │ │ ├── assets │ │ │ ├── images │ │ │ │ └── logo.svg │ │ │ ├── fonts │ │ │ └── scss │ │ ├── i18n │ │ ├── documentation │ │ ├── mock-data │ │ └── stories │ ├── index.scss │ ├── index.js │ ├── polyfill.js │ └── service-worker.js ├── .gitignore ├── .eslintrc ├── .eslintignore ├── .prettierrc ├── package.json ├── webpack.config.js └── README.md
En conclusión, esta estructura busca la flexibilidad en el crecimiento de la aplicación, así como soportar difrentes features de desarrollo, manteniendo los archivos lo más relacionado posible con su contexto y así favorecer a la transferencia de conocimiento sin incrementar la documentación.
Escribir este post me ha enseñado otras formas de estructurar las apps, así como herramientas y buenas prácticas. Por mi parte seguiré investigando a ver que más encuentro pero si tienen cualquier tipo de feedback, será bienvenido y tomado en cuenta para posteriores actualizaciones.