Uno de los mayores problemas en proyectos React medianos y grandes no es el código en sí — es la organización. Cuando el proyecto crece, una estructura improvisada se convierte en una deuda técnica que ralentiza a todo el equipo. Esta guía muestra cómo estructurar un proyecto React desde el principio para que escale sin convertirse en un laberinto.
El error más común: estructura plana
La mayoría de los proyectos empiezan así:
src/
components/
Header.jsx
Footer.jsx
UserCard.jsx
ProductList.jsx
ProductCard.jsx
CartItem.jsx
Modal.jsx
Button.jsx
... (50 componentes más)
pages/
Home.jsx
Products.jsx
Cart.jsx
App.jsx
Funciona para proyectos pequeños. En proyectos medianos, encontrar qué toca qué se vuelve imposible.
La estructura que escala: Feature-First
La idea central es agrupar el código por dominio de negocio, no por tipo técnico:
src/
features/
auth/
components/
LoginForm.jsx
RegisterForm.jsx
hooks/
useAuth.js
services/
authService.js
store/
authSlice.js
index.js ← API pública del feature
products/
components/
ProductCard.jsx
ProductList.jsx
ProductFilters.jsx
hooks/
useProducts.js
useProductSearch.js
services/
productService.js
index.js
cart/
components/
CartDrawer.jsx
CartItem.jsx
CartSummary.jsx
hooks/
useCart.js
store/
cartSlice.js
index.js
shared/
components/
Button/
Button.jsx
Button.module.css
Button.test.jsx
index.js
Modal/
Input/
hooks/
useDebounce.js
useLocalStorage.js
useMediaQuery.js
utils/
formatDate.js
formatCurrency.js
validators.js
constants/
routes.js
apiEndpoints.js
pages/
HomePage.jsx
ProductsPage.jsx
CartPage.jsx
layouts/
MainLayout.jsx
AuthLayout.jsx
lib/
apiClient.js ← instancia configurada de axios/fetch
queryClient.js ← React Query config
App.jsx
main.jsx
Las reglas que hacen que esto funcione
Regla 1: Los features solo se importan desde su index.js
Cada feature expone una API pública a través de su index.js. El resto del app no importa desde dentro del feature:
// ✅ Correcto
import { ProductCard, useProducts } from '@/features/products';
// ❌ Incorrecto — viola el encapsulamiento
import ProductCard from '@/features/products/components/ProductCard';
Esto te permite reorganizar el interior de un feature sin romper nada fuera de él.
Regla 2: Los features no se importan entre sí
Si el feature cart necesita datos de products, no importa directamente desde features/products. En su lugar, usa el store global o define una interfaz compartida en shared/:
// ❌ Crea dependencias circulares difíciles de gestionar
import { useProducts } from '@/features/products';
// ✅ Usa el store o una capa compartida
import { selectProductById } from '@/store/selectors';
Regla 3: Componentes compartidos en shared/, no en features
Si dos features usan el mismo componente, ese componente pertenece a shared/components/. Los features solo tienen componentes que son exclusivamente suyos.
Separación de capas dentro de un feature
products/
components/ ← Presentación pura: props in, JSX out
hooks/ ← Lógica y estado
services/ ← Llamadas a la API
store/ ← Estado global (Redux/Zustand)
Ejemplo concreto: hook separado del componente
// hooks/useProducts.js — solo lógica
import { useQuery } from '@tanstack/react-query';
import { productService } from '../services/productService';
export function useProducts(filters) {
return useQuery({
queryKey: ['products', filters],
queryFn: () => productService.getAll(filters),
staleTime: 5 * 60 * 1000,
});
}
// components/ProductList.jsx — solo presentación
import { useProducts } from '../hooks/useProducts';
import { ProductCard } from './ProductCard';
import { Skeleton } from '@/shared/components/Skeleton';
export function ProductList({ filters }) {
const { data: products, isLoading, error } = useProducts(filters);
if (isLoading) return <Skeleton count={6} />;
if (error) return <p>Error al cargar productos.</p>;
return (
<div className="product-grid">
{products.map(p => <ProductCard key={p.id} product={p} />)}
</div>
);
}
El componente no sabe nada de cómo se obtienen los datos. El hook no sabe nada de cómo se muestran.
Configurar alias de rutas
Con Vite, configura alias para evitar ../../../shared/components/Button:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
// jsconfig.json (para autocomplete en VS Code)
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
Cuándo aplicar esta estructura
No necesitas esto desde el día uno. Una estructura feature-first añade complejidad inicial que no se justifica para proyectos pequeños.
Empieza con la estructura plana si:
- Eres el único desarrollador
- El proyecto tiene menos de 20-30 componentes
- Es un prototipo o MVP
Migra a feature-first cuando:
- El equipo creció a 3+ desarrolladores
- Tienes más de 5 secciones/dominios distintos en el app
- El tiempo para encontrar archivos empieza a ser un problema
La buena noticia es que migrar gradualmente es posible: extrae un feature a la vez, empezando por el dominio más grande o más activo.