Estructura de proyecto en React: modularidad, organización y mantenibilidad

Capítulo 10

Tiempo estimado de lectura: 8 minutos

+ Ejercicio

Por qué la estructura del proyecto importa

Una buena estructura en React reduce el “costo de cambio”: encontrar archivos rápido, aislar responsabilidades, evitar duplicación y hacer que el código sea más fácil de probar. La idea central es organizar el proyecto para que el lector del código entienda dónde vive cada cosa (UI, lógica, estilos, assets) y por qué está ahí.

Estructura recomendada: carpetas por feature (dominio)

La organización por feature (también llamada por dominio) agrupa todo lo necesario para una funcionalidad en un mismo lugar: componentes, hooks locales, utilidades, estilos y assets. Esto mejora la modularidad y permite que cada feature evolucione con menos fricción.

Ejemplo de árbol de carpetas

src/  app/    App.tsx    routes.tsx    providers/      QueryProvider.tsx      ThemeProvider.tsx    layout/      MainLayout.tsx  features/    auth/      components/        LoginForm/          LoginForm.tsx          LoginForm.test.tsx          LoginForm.module.css          index.ts      hooks/        useLogin.ts      api/        authApi.ts      utils/        validators.ts      assets/        logo-auth.png      types.ts      index.ts    products/      components/        ProductCard/          ProductCard.tsx          ProductCard.module.css          index.ts      hooks/        useProducts.ts      api/        productsApi.ts      utils/        formatPrice.ts      types.ts      index.ts  shared/    components/      Button/        Button.tsx        Button.module.css        Button.test.tsx        index.ts      Modal/        Modal.tsx        Modal.module.css        index.ts    hooks/      useDebounce.ts    utils/      cn.ts      assertNever.ts    styles/      tokens.css      globals.css    assets/      icons/        cart.svg  lib/    http/      client.ts    storage/      localStorage.ts  main.tsx

Qué va en cada carpeta

  • app/: composición de alto nivel (rutas, layouts, providers). Evita meter lógica de negocio aquí.
  • features/<feature>/: todo lo específico del dominio (auth, products, checkout…).
  • shared/: piezas reutilizables y agnósticas del dominio (UI genérica, hooks genéricos, utilidades comunes, estilos globales).
  • lib/: infraestructura y adaptadores (cliente HTTP, storage, wrappers de APIs). Suele ser “sin UI”.

Guía práctica paso a paso para migrar a estructura por feature

Paso 1: identifica features reales

Lista dominios que tengan sentido para el usuario o el negocio: auth, products, cart, profile. Evita features demasiado genéricas como forms o helpers (eso suele ir a shared).

Paso 2: mueve componentes “cerca” de su dominio

Si un componente solo se usa en una feature, colócalo dentro de esa feature. Si se usa en varias, evalúa extraerlo a shared/components.

Paso 3: separa UI, lógica y acceso a datos dentro de la feature

  • components/: UI específica del dominio.
  • hooks/: lógica reutilizable dentro de la feature (por ejemplo, orquestación de llamadas, validaciones, estado derivado).
  • api/: funciones que hablan con el backend para esa feature.
  • utils/: funciones puras (formateo, validadores, mapeos).
  • types.ts: tipos del dominio (interfaces, unions).

Paso 4: define un “public API” por feature

Evita imports profundos y frágiles como features/auth/components/LoginForm/LoginForm. En su lugar, expón lo necesario desde un index.ts de la feature.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

// src/features/auth/index.ts
export { LoginForm } from './components/LoginForm';
export { useLogin } from './hooks/useLogin';
export type { AuthUser } from './types';

Y dentro del componente, usa un index.ts local:

// src/features/auth/components/LoginForm/index.ts
export { default as LoginForm } from './LoginForm';

Paso 5: crea reglas claras para “shared”

Una regla útil: shared no depende de features. Así evitas ciclos y acoplamientos. Si algo en shared empieza a necesitar tipos o lógica de una feature, probablemente no es shared.

Alternativa para proyectos pequeños: estructura por tipo

Cuando el proyecto es pequeño (pocas pantallas, un solo dominio), una estructura por tipo puede ser suficiente. La clave es no sobre-ingenierizar. Si crece, migrar a “por feature” suele ser natural.

src/  components/    Button/    Modal/  hooks/    useDebounce.ts  pages/    Home.tsx    Login.tsx  services/    httpClient.ts    authService.ts  utils/    format.ts  styles/    globals.css  assets/    icons/

Señales para migrar a “por feature”: carpetas components y services empiezan a llenarse con nombres de dominio (ProductCard, CartSummary, authService), o cuesta encontrar qué archivos cambian juntos.

Convenciones de nombres y co-locación

Nombres de archivos y carpetas

  • Componentes React: PascalCase para carpetas y archivos principales: ProductCard/ProductCard.tsx.
  • Hooks: camelCase con prefijo use: useProducts.ts.
  • Utilidades: verbos o intención clara: formatPrice.ts, mapApiProduct.ts.
  • Estilos: si usas CSS Modules: Component.module.css. Si usas otra solución, mantén consistencia por proyecto.

Co-locación recomendada

Coloca juntos los archivos que cambian juntos. Un patrón común por componente:

ProductCard/  ProductCard.tsx  ProductCard.module.css  ProductCard.test.tsx  ProductCard.stories.tsx (si aplica)  index.ts

Esto reduce el “salto” entre carpetas y facilita refactors.

Manejo de assets (imágenes, íconos, fuentes)

Reglas prácticas

  • Assets globales (logo general, íconos compartidos): src/shared/assets.
  • Assets específicos de una feature (imágenes solo de auth): src/features/auth/assets.
  • Evita una carpeta assets gigante sin estructura; termina siendo un “cajón desastre”.

Importación consistente

Mantén una convención: o bien importas assets desde módulos (recomendado en bundlers modernos) o los sirves desde una carpeta pública. Sea cual sea, documenta la regla y aplícala igual en todo el proyecto.

Patrones para componentes compartidos (shared)

1) Componentes UI “tontos” (presentacionales)

En shared/components, prioriza componentes con responsabilidades claras: renderizar UI a partir de props, sin conocer el dominio. Ejemplo: un botón reutilizable.

// src/shared/components/Button/Button.tsx
type ButtonProps = {
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
  onClick?: () => void;
  children: React.ReactNode;
};

export function Button({ variant = 'primary', disabled, onClick, children }: ButtonProps) {
  return (
    <button data-variant={variant} disabled={disabled} onClick={onClick}>
      {children}
    </button>
  );
}

2) “Adapters” de dominio fuera de shared

Si una feature necesita adaptar un componente shared a su dominio (por ejemplo, un Button con tracking o permisos), crea un wrapper dentro de la feature:

// src/features/products/components/AddToCartButton/AddToCartButton.tsx
import { Button } from '@/shared/components/Button';

type AddToCartButtonProps = {
  productId: string;
  onAdd: (productId: string) => void;
};

export function AddToCartButton({ productId, onAdd }: AddToCartButtonProps) {
  return <Button onClick={() => onAdd(productId)}>Añadir</Button>;
}

Así shared se mantiene genérico y la feature conserva su lógica.

3) Evita “shared” como basurero

Si algo solo se usa en un lugar, no lo subas a shared “por si acaso”. Extrae a shared cuando haya reutilización real o una intención clara de estandarización (por ejemplo, un sistema de diseño).

Pautas para componentes fáciles de probar y mantener

Props explícitas y contratos claros

Haz que el componente reciba lo que necesita por props, en lugar de leerlo de lugares implícitos. Esto mejora testabilidad y reusabilidad.

PreferibleEvitar

Recibir datos y callbacks por props

Depender de variables globales o imports con estado

Tipos de props concretos

Props “bolsa” tipo any o object

// Bien: contrato explícito
type PriceProps = { amount: number; currency: 'EUR' | 'USD' };
export function Price({ amount, currency }: PriceProps) {
  return <span>{currency} {amount.toFixed(2)}</span>;
}

Funciones puras cuando sea posible

Extrae lógica a funciones puras en utils/. Son más fáciles de testear porque no dependen del entorno.

// src/features/products/utils/formatPrice.ts
export function formatPrice(cents: number) {
  return (cents / 100).toFixed(2);
}

Luego el componente solo renderiza:

import { formatPrice } from '../utils/formatPrice';

export function ProductPrice({ cents }: { cents: number }) {
  return <span>€ {formatPrice(cents)}</span>;
}

Evitar dependencias implícitas

Dependencias implícitas típicas: leer directamente de localStorage dentro del componente, usar singletons globales, o acoplarse a rutas/servicios sin inyección. En su lugar, crea adaptadores en lib/ y pásalos como dependencias o encapsúlalos en funciones específicas.

// lib/storage/localStorage.ts
export const storage = {
  get(key: string) {
    return window.localStorage.getItem(key);
  },
  set(key: string, value: string) {
    window.localStorage.setItem(key, value);
  }
};
// feature: usar una función que puedas mockear en tests
import { storage } from '@/lib/storage/localStorage';

export function getAuthToken() {
  return storage.get('token');
}

Separar “container” y “presentational” cuando aporte claridad

Sin convertirlo en dogma: si un componente mezcla demasiada orquestación con UI, divide en:

  • Container: obtiene datos, llama hooks, decide qué pasar.
  • Presentational: recibe props y renderiza.
// Presentational
type LoginFormViewProps = {
  email: string;
  password: string;
  onEmailChange: (v: string) => void;
  onPasswordChange: (v: string) => void;
  onSubmit: () => void;
  isLoading: boolean;
  error?: string;
};

export function LoginFormView(props: LoginFormViewProps) {
  // render UI usando props
  return <div>...</div>;
}

Reglas de importación y límites entre módulos

Dependencias permitidas (sugerencia)

  • app puede importar de features, shared, lib.
  • features puede importar de shared y lib.
  • shared solo importa de shared y lib (idealmente).

Esto evita ciclos y mantiene una dirección clara del acoplamiento.

Aliases para imports más limpios

Configura un alias (por ejemplo @/ apuntando a src/) para evitar rutas relativas largas:

import { Button } from '@/shared/components/Button';
import { useProducts } from '@/features/products';

Ahora responde el ejercicio sobre el contenido:

Al migrar un proyecto React a una estructura por feature (dominio), ¿qué decisión ayuda a mejorar la modularidad y reducir imports frágiles?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

La estructura por feature agrupa lo específico del dominio (UI, hooks, acceso a datos y utilidades) y un index.ts permite importar desde una API pública, evitando rutas profundas y frágiles. Además, shared debe mantenerse agnóstico del dominio.

Siguiente capítulo

Errores comunes en React: diagnóstico y corrección orientada a conceptos

Arrow Right Icon
Portada de libro electrónico gratuitaReact para principiantes: mentalidad de componentes y manejo de estado
83%

React para principiantes: mentalidad de componentes y manejo de estado

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.