React 2023 - Como setupear un proyecto de forma optima

React 2023 - Como setupear  un proyecto de forma optima
Photo by Amr Taha™ / Unsplash

Introduccion

El mundo en el que vivimos cambia constantemente, y también lo hacen las librerías de desarrollo que usamos día a día, como React. Con la reciente caída de create-react-app, los usuarios de react deben optar por usar frameworks como next.js o gatsby, o moverse a una tool como vite. Cuando estos eventos ocurren, los devs se ven obligados a aprender una nueva estructura de proyecto, entonces porque no aprender a hacerlo de una forma optima?

Por qué es importante setupear bien un proyecto?

Una buena estructura de proyecto nos permite trabajar de forma más cómoda, lo que se traduce en trabajar más rápido y, por ende, ser más eficientes en nuestro trabajo. A continuación, te enumero el porqué deberías invertir tiempo en esto:

  • Una estructura de carpeta estandarizada nos permite trabajar más fácil en equipo.
  • Si estamos dentro de una empresa, estandarizar el setupeo en los proyectos reduce drásticamente el tiempo que le lleva a un desarrollador integrarse a un nuevo proyecto.
  • Tener linters y code-formatters nos permite mantener el código limpio y reforzar un estilo, lo que facilita mantener la legibilidad del código.
  • Reduce el tiempo que te lleva comenzar un nuevo proyecto.

1.  Creando la estructura base

Que herramientas o tecnologias vamos a usar para este setup?

  • React
  • Vite
  • Typescript
  • ESLint
  • Husky

En este articulo voy a utilizar vite dado que es la herramienta másparecida a create-react-app ya que next.js y gatsby son frameworks completos.  

Uso typescript en todos mis proyectos, por varios motivos:

  • Codigo mas declarativo, por ende mas escalable.
  • El intellisense funciona mucho mejor que con javascript.
  • Todo es más fácil con tipados definidos, se pueden agarrar muchos errores en tiempo de diseño, ahorrándote la necesidad de levantar el proyecto y probarlo.

Empezemos por el armar la estructura base utilizando la herramienta de vite


npm create vite@latest <project-name> -- --template react-ts

El resultado:

Estructura de proyecto con vite

2. Estructura de carpetas

La estructura de carpeta que voy a proponer es la que usamos en la mayoria de los proyectos de Skillful Dev con React.

Estructura de carpeta sugerida

Vamos a explicar el porque de cada carpeta dentro de src.

Siempre digo que lo importante no es el "qué" sino mas bien el "porqué". Lo importante es que tengas algun motivo por el cual lo haces de esa manera.
  • assets Es donde guardamos nuestros svg, imagenes, sonidos, etc. Es decir todo asset estáticoque necesitamos para nuestra app.
  • components Aquí ponemos todos los components de react
  • components/context Cualquier React.Context que creemos lo metemos en esta carpeta. Lo ideal es un context por archivo.
  • components/hooks Aquí lo mismo, cualquier custom hook que creemos lo ponemos en esta carpeta, podemos separar uno por archivo o como creamos conveniente segun la necesidad del proyecto.
  • components/layouts Probablemente la carpeta más complicada de explicar. Acá suelo guardar el diseño o estructura base de la página. Por ejemplo, si mi app renderiza una vista para usuarios finales donde muestra una navbar a la izquierda y un footer, y para los usuarios administradores solo renderiza una navbar arriba, ahí tengo dos layouts distintos que puedo abstraer y reutilizar.
  • components/routes Aquí suelo tener un archivo donde combino todas las rutas de la app, y un archivo donde tengo los types de las rutas, el cual mostraré más adelante en el artículo.
  • components/shared Todos los componentes reutilizables que creemos van en esta carpeta, la cual se suele subdivdir por vistas  o por tipo de componente (ej: buttons, modals)
  • components/views Para quienes conocen next.js esto es lo mismo que  la carpeta pages, cada componente que represente una vista debe ir acá. y lo suelo separar en dos partes: vistas privadas (aquellas que requiren haber iniciado sesión) y vistas públicas (cualquier vista que sea de público acceso).
  • config Configuración que necesitemos, sea la instancia de axios para conectarse al backend, una configuración particular para una librería como react-query o una configuración para un provider de blockchain como wagmi recomiendo hacerla dentro de esta carpeta, de esta forma cuando haya que cambiar un parámetro ya saben que la configuración se ubica en alguno de los archivos de esta carpeta.
  • data Esta carpeta es opcional, la solemos usar para mockear data para testear los componentes
  • services Las llamadas a las APIs deben ir acá, sugiero separar un archivo por resource, ejemplo: user.services.ts, post.services.ts
  • styles Nosotros usamos material-ui y styled components por ende esta carpeta la utilizamos para definir un theme global, normalizar estilos, etc.
  • types Los tipados que utilizamos para los modelos de datos que manejamos los guardamos en esta carpeta.
  • utils También se suele llamar helpers. Cualquier función de ayuda que no posea lógica de negocio debe ir acá. Por ejemplo: una función para formatear la fecha, o una clase que simplifica el acceso al local storage para guardar las credenciales.
  • validations Todos los esquemas de validación que utilizamos en los forms los guardamos acá, siguiendo la misma lógica que en services, un archivo por resource: user.dto.ts, post.dto.ts

3. Configurando ESLint

Vite por defecto nos instala ESLint por lo cual hay que agregar unas extensiones unicamente.

npm i -D eslint-plugin-jsx-a11y 
npm i -D @typescript-eslint/eslint-plugin
npm i -D @typescript-eslint/parser

Nuestra configuración de ESLint extiende las rules de Airbnb, y modificamos algunas cosas a gusto. Los estilos son bastante personales, lo importante es ser consistente. Si tienes un equipo, lo mejor es que se pongan de acuerdo y respeten esa configuración para todos los proyectos.

module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    project: 'tsconfig.json',
    sourceType: 'module',
  },
  plugins: ['@typescript-eslint/eslint-plugin'],
  extends: [
    'plugin:@typescript-eslint/eslint-recommended',
    'plugin:@typescript-eslint/recommended',
    'airbnb-typescript/base'
  ],
  root: true,
  env: {
    node: true,
    jest: true,
  },
  rules: {
    '@typescript-eslint/interface-name-prefix': 'off',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-explicit-any': 'off',
    '@typescript-eslint/comma-dangle': 'off',
    '@typescript-eslint/no-loop-func': 'off',
    '@typescript-eslint/no-redeclare': 'off',
    '@typescript-eslint/no-shadow': 'off',
    '@typescript-eslint/space-infix-ops': 'off',
    '@typescript-eslint/object-curly-spacing': 'off',
    'prefer-destructuring': 'off',
    'import/prefer-default-export': 'off',
    'class-methods-use-this': 'off',
    'max-classes-per-file': 'off',
    'no-param-reassign': 'off',
    'no-restricted-syntax': 'off',
    'no-continue': 'off',
    'max-len': ['error', 120],
    semi: ['error', 'never'],
    '@typescript-eslint/semi': ['error', 'never'],
    'no-console': 'off',
    'import/no-cycle': 'off',
    '@typescript-eslint/no-unused-vars': ['error'],
    'import/no-extraneous-dependencies': 'off',
    'no-case-declarations': 'off',
    '@typescript-eslint/no-use-before-define': 'off',
    'import/extensions': 'off',
    'import/no-named-as-default': 'off',
    'no-multi-assign': 'off',
    '@typescript-eslint/no-non-null-assertion': 'off',
    '@typescript-eslint/no-empty-interface': 'off',
    'consistent-return': 'off',
    'func-names': 'off',
  }
}

4. Integrando ESLint con VSCODE

Lo mejor es tener configurado VSCode por workspace (espacio de trabajo). De esta forma, si trabajamos en distintos proyectos, toma la configuración de cada proyecto por encima de la global.

Se puede configurar vs code para que ejecute el eslint fix al guardar y así tener un code formatter. Para ello, creamos una carpeta .vscode en el root de nuestro proyecto y agregamos un archivo llamado settings.json con la siguiente configuración

{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "editor.detectIndentation": false,
  "editor.insertSpaces": true,
  "editor.tabSize": 2,
}

5. Correr ESLint antes de cada commit

Existe una herramienta muy útil llamada husky que nos permite ejecutar una acción antes de ciertas acciones o procesos. Yo la utilizo para que al hacer git commit corra los tests y el ESLint formateando el código, y en caso de que haya un error que no permita el commit.

Para configurarla, primero debemos instalar estas dependencias:

npm i -D husky
npm i -D lint-staged

Adicionalmente en el package.json deberemos agregar un script:

{
    ...
    "prepare": "husky install"
    ...
}

Debemos crear un archivo .lintstagedrc y colocar la siguiente configuración

{
  "*.ts": "eslint --fix",
  "*.tsx": "eslint --fix"
}

Luego debemos correr el script npm run prepare, lo cual nos creara una carpeta .husky en el root del proyecto. dentro de esa carpeta creamos un archivo llamado pre-commit con lo siguiente:

#!/usr/bin/env sh
. “$(dirname -- “$0")/_/husky.sh”
npx lint-staged

Y listo, cada vez que hagamos git commit correra el ESLint sobre los archivos modificados (lo cual es mucho más rapido que correrlo sobre todo el proyecto, esto es gracias a lint-staged)


6. Buenas practicas

Rutas

Cuántas veces nos ha pasado que no entendemos por qué no se renderiza un componente y es por un typo en el nombre de la ruta? Una buena práctica para evitar esto es no hardcodear las rutas, definiéndolas en un enum:

// routes/routes.types.ts

export enum ELinks {
    Home = '/home',
    About = '/about',
    Users = '/users',
    UserForm = '/user-form'
}

Que poner en el App.tsx?

Muchas veces me encuentro con proyectos donde el archivo que une toda nuestra app tiene rutas, estilos, lógica de negocio, y hasta API calls. Mi recomendación es dejarlo lo más limpio posible.

import { StyledEngineProvider, ThemeProvider } from '@mui/material/styles'
import { QueryClient, QueryClientProvider } from 'react-query'

import { theme } from './styles/theme'
import { AppNavigation } from './components/routes/navigation'
import { AuthWrapper } from './components/shared/AuthWrapper'
import { queryClient } from './config/query-client.config.ts'

const App = () => (
  <QueryClientProvider client={queryClient}>
    <StyledEngineProvider injectFirst>
      <ThemeProvider theme={theme}>
        <AuthWrapper>
          <AppNavigation />
        </AuthWrapper>
      </ThemeProvider>
    </StyledEngineProvider>
  </QueryClientProvider>
)
export default App

En este archivo suelo poner los Providers o Wrappers de más alto nivel, como el provider de Material-UI que me permite acceder a los estilos en todos los componentes, o la configuración global de query client y el componente root de las rutas, que une todas las rutas de la app. De esta forma, la lógica que suele cambiar no se encuentra dentro de este archivo y es algo menos por lo que me tengo que preocupar.

Conclusion

Hemos visto cómo transformar la estructura base que nos provee Vite en una de mayor calidad que nos provee unos cimientos sólidos para escalar la codebase de nuestra app.

Hay muchas formas de configurar un proyecto, los invito a que me cuenten en los comentarios qué setups usan ustedes, y por supuesto, estoy abierto a cualquier mejora que sugieran!


Acerca de mi

Buenas! Este es mi primer artículo, por lo que me presento. Soy Liber, un Ingeniero en Sistemas con casi 6 años de experiencia en el área de software. Soy founder de Skillful Dev, una empresa de desarrollo de software y, además del management, suelo desarrollar como fullstack, ayudando donde se necesite. Gracias por leer! :D