Skip to content

Estructura de aplicaciones React

Patrones de organización para componentes React. Cómo estructurar código para que escale sin volverse un caos.

Estructura de carpetas

Mi estructura actual para proyectos React:

src/
├── components/       # Componentes reutilizables
│   ├── Button.jsx
│   ├── Button.css
│   ├── Card.jsx
│   └── Card.css
├── pages/            # Vistas completas (rutas)
│   ├── HomePage.jsx
│   ├── HomePage.css
│   ├── LoginPage.jsx
│   └── LoginPage.css
├── context/           # Estado global
│   └── AuthContext.jsx
├── hooks/             # Lógica reutilizable
│   ├── useAuth.js
│   └── useFetch.js
├── services/          # Llamadas API
│   └── api.js
├── utils/             # Funciones auxiliares
│   └── formatDate.js
├── App.jsx
├── App.css
├── index.css
└── main.jsx

Componentes presentacionales vs containers

Presentacionales: solo renderizan, no tienen lógica de negocio.

jsx
// components/Button.jsx
const Button = ({ children, onClick, variant = 'primary' }) => {
  return (
    <button className={`btn btn-${variant}`} onClick={onClick}>
      {children}
    </button>
  );
};

Containers: conectan con datos, manejan estado.

jsx
// containers/GameList.jsx
const GameList = () => {
  const [games, setGames] = useState([]);

  useEffect(() => {
    api.get('/games').then(setGames);
  }, []);

  return <div className="games-grid">{games.map(g => <GameCard key={g.id} game={g} />)}</div>;
};

Custom Hooks para lógica reutilizable

Extraer lógica a hooks mantiene los componentes limpios.

Ejemplo: useAuth

javascript
// hooks/useAuth.js
import { useContext } from 'react';
import { AuthContext } from '../context/AuthContext';

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
};

// Uso en cualquier componente
const GameCard = ({ game }) => {
  const { user, logout } = useAuth();
  return <div>...</div>;
};

Ejemplo: useFetch

javascript
// hooks/useFetch.js
import { useState, useEffect } from 'react';

export const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url);
        if (!response.ok) throw new Error('Network error');
        setData(await response.json());
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [url]);

  return { data, loading, error };
};

Props vs Context vs Estado local

Cuándo usarEjemplo
Estado local (useState)Toggle de un modal, input de un formulario
PropsDatos que vienen del padre, callbacks
ContextAuth global, tema (dark/light), lenguaje
Estado en URL (useParams)ID del recurso /games/:id

Regla general: no uses Context para todo. Si solo un componente necesita un valor, props basta.

Composición vs Prop drilling

Prop drilling (evitar en casos profundos)

jsx
// App.jsx
<UserProvider>
  <App>
    <Dashboard>
      <Sidebar>
        <UserAvatar />  // Solo para mostrar nombre del usuario
      </Sidebar>
    </Dashboard>
  </App>
</UserProvider>

Composición (mejor)

jsx
// Componente que recibe children
const Layout = ({ children }) => {
  return <div className="layout">{children}</div>;
};

// Uso: composición en vez de props innecesarios
<Layout>
  <Sidebar>
    <UserAvatar user={user} />
  </Sidebar>
</Layout>

Servicios API centralizados

En vez de hacer fetch dispersos por toda la app:

javascript
// services/api.js
const API_URL = import.meta.env.VITE_API_URL;

export const api = {
  get: (path) => fetch(`${API_URL}${path}`).then(r => r.json()),
  post: (path, body) => fetch(`${API_URL}${path}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body)
  }).then(r => r.json()),
  delete: (path) => fetch(`${API_URL}${path}`, { method: 'DELETE' }).then(r => r.ok)
};

// Uso
const games = await api.get('/games');
await api.post('/games', { title: 'New Game' });

Patrones que evitar

No: todo en App.jsx

jsx
// ❌ NO
const App = () => {
  const [user, setUser] = useState(null);
  const [games, setGames] = useState([]);
  const [theme, setTheme] = useState('dark');
  // 500 líneas de lógica...
};

Sí: separar responsabilidades

jsx
// ✅ SÍ
const App = () => {
  return (
    <AuthProvider>
      <ThemeProvider>
        <Router>
          <Routes>
            <Route path="/games" element={<GamesPage />} />
          </Routes>
        </Router>
      </ThemeProvider>
    </AuthProvider>
  );
};

Archivos de barril (index.js)

Exportar todo desde un archivo central para imports limpios:

javascript
// hooks/index.js
export { useAuth } from './useAuth';
export { useFetch } from './useFetch';

// Uso: un solo import
import { useAuth, useFetch } from '../hooks';

La estructura de React no es dogma—adáptala a tu proyecto. Un proyecto de 3 componentes no necesita 6 carpetas. Pero cuando crece, una buena estructura paga dividends.

Más guías y proyectos en mmoreno.dev.