🚀 ¡Las clases del Bootcamp vuelven el 7 de enero con Node.js! Cargando...

Introducción a React Context

Por qué necesitamos React Context

En aplicaciones React, muchas veces necesitamos compartir datos entre componentes que están lejos en el árbol. La forma tradicional ha sido pasar props desde un padre a un hijo… y luego al hijo… y luego al hijo del hijo.

El problema del Prop Drilling

Imagina que tienes esta estructura de componentes y necesitas pasar el estado de autenticación desde el componente raíz hasta un botón profundo en el árbol:

           App (tiene isLogin)
            |
         Header (recibe isLogin como prop)
            |
         Navbar (recibe isLogin como prop)
            |
        UserMenu (recibe isLogin como prop)
            |
      UserButton (FINALMENTE usa isLogin)

Problema: Los componentes intermedios (Header, Navbar, UserMenu) no necesitan isLogin, pero tienen que recibirlo y pasarlo hacia abajo. Esto se llama prop drilling.

Esto provoca:

  • Prop drilling: Pasar props a través de múltiples niveles innecesariamente
  • Código difícil de mantener: Cualquier cambio requiere actualizar muchos componentes
  • Dependencias innecesarias: Componentes acoplados que no deberían estarlo
  • Refactorización compleja: Mover un componente implica revisar toda la cadena

La solución: React Context

React nos ofrece una solución integrada: Context, una API para crear un estado global accesible desde cualquier componente sin necesidad de pasar props manualmente.

Con Context, la estructura se simplifica:

           App
            |
    <AuthProvider> (provee isLogin)
            |
         Header
            |
         Navbar
            |
        UserMenu
            |
      UserButton (accede directamente a isLogin)

Ventaja: UserButton puede acceder directamente a isLogin sin que los componentes intermedios se enteren. Es como tener un “túnel” directo desde el Provider hasta el consumidor.

En esta clase refactorizamos una aplicación para utilizar Context y simplificar toda la gestión de autenticación.


Creación del Context

Lo primero es crear una carpeta nueva para organizar nuestros contextos:

/src/context

Dentro creamos authContext.jsx, donde definiremos todo lo relacionado con la autenticación. Allí importamos las herramientas necesarias: createContext y useState.

import { createContext, useState } from 'react'

export const AuthContext = createContext()

¿Qué es createContext?

createContext() crea un “contenedor” de datos global. Piénsalo como una emisora de radio: crea el canal, pero todavía no emite nada.

createContext() → Crea el "canal"

AuthContext → El canal está listo

Necesitamos un Provider → Para empezar a "emitir"

Los componentes usan useContext → Para "sintonizar" el canal

Con esto ya tenemos un contexto, pero todavía no hace nada. Necesitamos su Provider: el componente que envuelve la app y provee los valores globales.


Creando el Provider

El Provider es el componente que “emite” los datos. Todos los componentes que estén dentro del Provider podrán acceder a esos datos.

Creamos el componente AuthProvider:

export function AuthProvider({ children }) {
  const [isLogin, setIsLogin] = useState(false)

  function login() {
    setIsLogin(true)
  }
  function logout() {
    setIsLogin(false)
  }

  const value = { isLogin, login, logout }

  return <AuthContext value={value}>{children}</AuthContext>
}

Anatomía del Provider

AuthProvider

 [Estado interno]
 - isLogin (estado)
 - login (función)
 - logout (función)

 [Empaqueta todo en 'value']
 { isLogin, login, logout }

 [Lo provee vía Context]
 <AuthContext value={...}>

 [Renderiza los hijos]
 {children} ← Toda tu app puede acceder al value

Este componente hace tres cosas clave:

  1. Mantiene el estado global: Usa useState para isLogin
  2. Expone funciones para modificarlo: login y logout para cambiar el estado
  3. Envuelve a cualquier parte de la app: Todo lo que esté dentro de {children} puede acceder al contexto

Nota importante: React 18 permite usar el contexto directamente sin .Provider, lo cual simplifica la sintaxis. En versiones anteriores tendrías que escribir <AuthContext.Provider>.


Usando el Provider en la aplicación

Ahora vamos al punto de entrada (normalmente main.jsx o App.jsx) y envolvemos toda nuestra aplicación:

<AuthProvider>
  <App />
</AuthProvider>

Visualización del alcance del Provider

main.jsx
    |
<AuthProvider> ← Inicia la "señal"
    |
    ├─ <App>
    |   ├─ <Header>      ← Puede acceder
    |   |   └─ <Nav>     ← Puede acceder
    |   |       └─ <UserButton> ← Puede acceder
    |   |
    |   ├─ <Routes>
    |   |   ├─ <Home>    ← Puede acceder
    |   |   ├─ <Detail>  ← Puede acceder
    |   |   └─ <About>   ← Puede acceder
    |   |
    |   └─ <Footer>      ← Puede acceder
    |
└─ </AuthProvider>

Concepto clave: Todo componente que esté dentro de <AuthProvider> puede acceder al estado de autenticación. Es como si el Provider creara una “zona WiFi” y todos los componentes dentro pueden conectarse.

Esto asegura que toda la app tenga acceso al estado global de autenticación sin necesidad de pasar props.


Consumir el contexto con useContext

Antes, pasábamos el estado y las funciones por props al Header o a JobDetailPage.

Eso ya no hace falta.

El hook useContext

Usamos el hook useContext para “sintonizar” el canal y acceder a los datos:

import { useContext } from 'react'
import { AuthContext } from '../context/authContext'

const { isLogin, login, logout } = useContext(AuthContext)

Comparación: Antes vs Después

ANTES (con prop drilling):

// App.jsx
function App() {
  const [isLogin, setIsLogin] = useState(false)

  return <Header isLogin={isLogin} onLogin={() => setIsLogin(true)} />
}

// Header.jsx
function Header({ isLogin, onLogin }) {
  return <Nav isLogin={isLogin} onLogin={onLogin} />
}

// Nav.jsx
function Nav({ isLogin, onLogin }) {
  return <UserButton isLogin={isLogin} onLogin={onLogin} />
}

// UserButton.jsx
function UserButton({ isLogin, onLogin }) {
  return <button onClick={onLogin}>{isLogin ? 'Salir' : 'Entrar'}</button>
}

DESPUÉS (con Context):

// App.jsx
function App() {
  return <Header /> // ✨ Sin props
}

// Header.jsx
function Header() {
  return <Nav /> // ✨ Sin props
}

// Nav.jsx
function Nav() {
  return <UserButton /> // ✨ Sin props
}

// UserButton.jsx
function UserButton() {
  const { isLogin, login } = useContext(AuthContext) // ✨ Acceso directo
  return <button onClick={login}>{isLogin ? 'Salir' : 'Entrar'}</button>
}

Ventajas visibles

PROP DRILLING          vs          CONTEXT
─────────────                      ────────

App (define)                       App
 ↓ (pasa props)                     |
Header (no usa)              <AuthProvider> (define)
 ↓ (pasa props)                     |
Nav (no usa)                      Header
 ↓ (pasa props)                     |
UserButton (usa)                  Nav
                                    |
                               UserButton (usa) ← useContext()

5 componentes afectados            2 componentes afectados

Ahora cualquier componente puede acceder al estado global sin depender del árbol ni recibir props innecesarias.

Ejemplo aplicado al Header:

  • ✅ Eliminamos props como isLogin, onLogin, onLogout
  • ✅ Leemos directamente desde el contexto
  • ✅ Los componentes intermedios ya no se acoplan al estado
  • ✅ Más fácil de refactorizar y mover componentes

Eliminando props innecesarias

El Detail ya no recibe props de autenticación.

En lugar de:

<JobDetailPage isLogin={isLogin} />

Simplemente:

<JobDetailPage />

y dentro:

const { isLogin } = useContext(AuthContext)

De esta forma desaparece el prop drilling y se simplifica el código de forma notable.


El problema con los re-renderizados

Después de implementar Context, aparece un detalle interesante:

Al actualizar el estado del Provider, se vuelve a renderizar toda la aplicación.

¿Por qué ocurre esto?

Porque el Provider está en lo más alto del árbol y su estado cambia. Cuando el estado del Provider cambia, React re-renderiza el Provider y todos sus hijos.

<AuthProvider>  ← Estado cambia aquí (isLogin: false → true)
    |
    ├─ <Header>      🔄 Se re-renderiza
    |   └─ <Nav>     🔄 Se re-renderiza
    |       └─ <UserButton> 🔄 Se re-renderiza (OK, necesita actualizarse)
    |
    ├─ <Routes>      🔄 Se re-renderiza (innecesario)
    |   ├─ <Home>    🔄 Se re-renderiza (innecesario)
    |   └─ <Detail>  🔄 Se re-renderiza (innecesario)
    |
    └─ <Footer>      🔄 Se re-renderiza (innecesario)

Es normal, pero tiene consecuencias:

  • Componentes que no usan el contexto también se re-renderizan
  • El header completo se recalcula aunque solo el botón de usuario necesite actualizarse
  • En apps grandes, esto puede afectar al rendimiento

Optimización: Dividir componentes

La solución es separar lo que depende del contexto de lo que no:

ANTES:

function Header() {
  const { isLogin, login, logout } = useContext(AuthContext)

  return (
    <header>
      <Logo />
      <Nav />
      <SearchBar />
      {isLogin ? (
        <button onClick={logout}>Cerrar sesión</button>
      ) : (
        <button onClick={login}>Iniciar sesión</button>
      )}
    </header>
  )
}

Todo el Header se re-renderiza cuando cambia isLogin.

DESPUÉS:

function Header() {
  return (
    <header>
      <Logo />
      <Nav />
      <SearchBar />
      <UserButton /> {/* ← Componente separado */}
    </header>
  )
}

function UserButton() {
  const { isLogin, login, logout } = useContext(AuthContext)

  return isLogin ? (
    <button onClick={logout}>Cerrar sesión</button>
  ) : (
    <button onClick={login}>Iniciar sesión</button>
  )
}

Ahora solo UserButton se re-renderiza:

<Header>  ⚪ No se re-renderiza (no usa Context)
    ├─ <Logo>      ⚪ No se re-renderiza
    ├─ <Nav>       ⚪ No se re-renderiza
    ├─ <SearchBar> ⚪ No se re-renderiza
    └─ <UserButton> 🔄 Se re-renderiza (usa Context)

Regla de oro: Si solo una pequeña parte del componente usa el contexto, extráela a un componente hijo.

Limitaciones de Context

Context es genial para casos simples, pero tiene limitaciones:

  • ❌ Re-renderiza todo aunque solo uses una propiedad del contexto
  • ❌ No tiene optimizaciones avanzadas de rendimiento
  • ❌ Difícil de debuggear en apps grandes
  • ❌ No tiene devtools especializadas

Más adelante mejoraremos aún más esto usando Zustand, una librería que soluciona estas limitaciones manteniendo la simplicidad.


Resumen de la clase

En esta clase has aprendido:

  1. Qué es React Context y para qué se usa
  2. Cómo crear un Context y un Provider
  3. Cómo envolver tu app para habilitar el estado global
  4. Cómo consumir el contexto desde cualquier componente usando useContext
  5. Cómo eliminar props innecesarias y reducir prop drilling
  6. Por qué el Provider provoca re-renderizados globales y cómo minimizarlo separando componentes
  7. Qué limitaciones tiene Context y por qué herramientas como Zustand pueden ser mejores para casos avanzados

Conclusión

React Context es una herramienta fundamental para crear estado global sin bibliotecas externas.

Refactorizar tu aplicación con Context elimina complejidad, evita props profundas y te prepara para arquitecturas más escalables.

En la siguiente clase veremos cómo evitar re-renderizados innecesarios y cómo introducir librerías más eficientes para gestión de estado global.