Props drilling: cuándo deja de ser tolerable en MERN
Tres componentes para una prop que dos no usan. Identificá el punto exacto donde props drilling deja de ser tolerable y qué patrones lo resuelven en MERN.
Equipo NUCBA
Props drilling: cuándo deja de ser tolerable en MERN
Pasaste tres componentes para enviar un userId y ahora necesitás agregar userRole. Eso significa tocar cuatro archivos para una prop que dos componentes ni siquiera usan. Props drilling arranca como algo tolerable y termina siendo el bug más caro de tu proyecto: no rompe nada, solo hace que cada cambio tarde el triple.
En aplicaciones MERN, props drilling se vuelve visible rápido. React en el frontend y Node/Express en el backend crean un stack donde la data fluye de arriba hacia abajo, pero nadie te dice cuándo ese flujo empieza a estrangular tu arquitectura. Este artículo identifica las señales exactas donde props drilling deja de ser aceptable y qué patrones lo resuelven sin romper la estructura del stack.
La métrica que importa: cuántos componentes no usan la prop
Props drilling no es malo porque sí. Es malo cuando componentes intermedios se convierten en cables de extensión: reciben props que nunca usan, solo para pasarlas más abajo.
El punto de quiebre: si una prop atraviesa más de dos componentes intermedios sin ser usada, ya tenés un problema de mantenimiento. No es una regla teórica, es lo que se mide en code reviews de equipos que escalan.
Ejemplo típico en MERN:
// App.js function App() { const [user, setUser] = useState(null); return <Dashboard user={user} />; } // Dashboard.js function Dashboard({ user }) { return ( <div> <Sidebar user={user} /> <MainContent user={user} /> </div> ); } // Sidebar.js function Sidebar({ user }) { return ( <nav> <Navigation user={user} /> </nav> ); } // Navigation.js (acá FINALMENTE se usa) function Navigation({ user }) { return <div>Hola, {user.name}</div>; }
Dashboard y Sidebar no hacen nada con user. Solo lo pasan. Eso es props drilling tolerable hasta que necesitás agregar userPermissions, userSettings, y userPreferences. Ahora tenés cuatro props atravesando tres componentes. Cada cambio en la API de usuario requiere tocar seis archivos.
Checklist para detectar props drilling insostenible:
- Una prop atraviesa más de 2 niveles sin ser usada
- Tenés 3 o más props relacionadas pasándose juntas (señal de que deberían ser contexto)
- Agregar una nueva prop requiere editar más de 3 archivos
- Los componentes intermedios tienen 5+ props que solo pasan hacia abajo
- Tu equipo evita agregar features porque "hay que tocar todo"
Context API: el primer patrón que deberías probar
Context API es la solución nativa de React para props drilling. No es la más sexy ni la más performante, pero es la más directa para la mayoría de casos en MERN.
Cuándo usarlo: cuando tenés data de "aplicación" (user, auth, theme, idioma) que muchos componentes necesitan pero pocos modifican.
// contexts/UserContext.js import { createContext, useContext, useState, useEffect } from 'react'; const UserContext = createContext(); export function UserProvider({ children }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { // Llamada a tu API de Express fetch('/api/auth/me') .then(res => res.json()) .then(data => { setUser(data.user); setLoading(false); }); }, []); return ( <UserContext.Provider value={{ user, setUser, loading }}> {children} </UserContext.Provider> ); } export function useUser() { const context = useContext(UserContext); if (!context) { throw new Error('useUser debe usarse dentro de UserProvider'); } return context; }
Ahora cualquier componente accede directo:
// Navigation.js import { useUser } from '../contexts/UserContext'; function Navigation() { const { user, loading } = useUser(); if (loading) return <Spinner />; return <div>Hola, {user.name}</div>; }
Dashboard y Sidebar desaparecen de la ecuación. Agregar userPermissions significa tocar UserContext y los componentes que lo usan. No los intermedios.
Advertencia real: Context API re-renderiza todos los componentes que consumen el contexto cuando cualquier valor del provider cambia. Si tenés un contexto con { user, notifications, settings } y cambiás notifications, todo lo que use user también se re-renderiza. La solución: dividir contextos por dominio o usar useMemo en el value del provider.
Composition: el patrón que evita que necesites Context
Props drilling muchas veces es síntoma de mala composición de componentes. Antes de agregar Context, probá si composition resuelve el problema.
Ejemplo: en lugar de pasar user por tres niveles para llegar a un avatar, pasá el componente avatar ya renderizado.
Antes:
function App() { const user = useUser(); return <Dashboard user={user} />; } function Dashboard({ user }) { return <Header user={user} />; } function Header({ user }) { return <UserAvatar user={user} />; }
Después (composition):
function App() { const user = useUser(); return ( <Dashboard header={<Header avatar={<UserAvatar user={user} />} />} /> ); } function Dashboard({ header }) { return <div>{header}</div>; } function Header({ avatar }) { return <header>{avatar}</header>; }
Dashboard y Header ya no saben nada de user. Reciben componentes ya configurados. Este patrón se llama "component composition" o "children as props" y es increíblemente subestimado.
Cuándo usarlo: cuando la prop es específica de un componente hoja y no es "data global". Avatares, botones personalizados, secciones condicionales. Composition es mejor que Context para estos casos porque no agrega overhead de providers ni re-renders.
Redux/Zustand: cuándo el problema es de estado, no de props
Si Context API empieza a dar problemas de performance o tenés demasiados contextos anidados, el problema dejó de ser props drilling y se convirtió en gestión de estado.
Señales de que necesitás una librería de estado:
- Tenés más de 3 contextos en tu árbol de componentes
- Context re-renderiza demasiado y
useMemono alcanza - Necesitás middleware (logging, persistencia, side effects complejos)
- Múltiples componentes modifican el mismo estado de formas distintas
Zustand es una alternativa ligera a Redux que se integra bien en MERN:
// store/userStore.js import create from 'zustand'; export const useUserStore = create((set) => ({ user: null, loading: true, setUser: (user) => set({ user, loading: false }), logout: () => set({ user: null }), })); // En tu componente import { useUserStore } from '../store/userStore'; function Navigation() { const user = useUserStore(state => state.user); const logout = useUserStore(state => state.logout); return <button onClick={logout}>Salir</button>; }
La ventaja sobre Context: los componentes solo se re-renderizan si la porción exacta del estado que consumen cambia. useUserStore(state => state.user) no re-renderiza si cambia loading.
Redux vs Zustand en MERN: Redux tiene mejor ecosistema (devtools, middlewares, Redux Toolkit), Zustand tiene menos boilerplate. Para la mayoría de proyectos MERN, Zustand alcanza. Redux cuando tu estado necesita time-travel debugging o tenés lógica de negocio compleja en el frontend.
El patrón "render props" para casos puntuales
Render props es útil cuando necesitás pasar comportamiento, no solo datos, sin agregar un Context completo.
// DataFetcher.js function DataFetcher({ url, render }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { fetch(url) .then(res => res.json()) .then(data => { setData(data); setLoading(false); }); }, [url]); return render({ data, loading }); } // Uso function UserProfile() { return ( <DataFetcher url="/api/users/me" render={({ data, loading }) => loading ? <Spinner /> : <Profile user={data} /> } /> ); }
Es menos común que Context o Zustand, pero útil para componentes de lógica reutilizable (fetching, formularios, animaciones).
Checklist: qué patrón usar según el caso
- Props drilling tolerable (1-2 niveles): Dejalo así. No agregues complejidad innecesaria.
- Data de aplicación (user, auth, theme): Context API o Zustand.
- Props específicas de componentes hoja: Composition (children/render props).
- Estado complejo con muchas mutaciones: Zustand o Redux.
- Lógica reutilizable sin estado global: Render props o custom hooks.
- Más de 3 contextos anidados: Migrá a Zustand/Redux antes de que sea tarde.
Preguntas frecuentes
¿Context API es más lento que Redux?
En términos de rendering, Context puede ser más lento porque no tiene optimizaciones de suscripción granular por defecto. Redux (especialmente con Redux Toolkit) y Zustand solo re-renderizan componentes que usan la porción exacta del estado que cambió. En la práctica, para apps MERN pequeñas-medianas, la diferencia es imperceptible. Context es más lento cuando tenés contextos enormes con muchos consumidores.
¿Puedo mezclar Context y Zustand en el mismo proyecto?
Sí, y es común. Context para cosas estables (theme, idioma, auth básico) y Zustand para estado que cambia seguido (carritos, notificaciones, filtros). No hay conflicto técnico. El riesgo es que tu equipo no sepa dónde poner cada cosa. Definí reglas claras: "auth va en Context, features van en Zustand".
¿Props drilling es siempre un anti-pattern?
No. Pasar 1-2 niveles de props es completamente normal y mantiene el código explícito. El problema es cuando se vuelve mecánico: pasás props sin pensar porque "así es la estructura". Ahí es donde composition o Context salvan tiempo. La regla: si tocar un prop requiere editar más de 3 archivos, buscá otra solución.