Inicio / TypeScript / React: Frontend Moderno con TypeScript / Hooks Avanzados: useReducer, useContext, useRef, useMemo, useCallback

Hooks Avanzados: useReducer, useContext, useRef, useMemo, useCallback

useReducer para estado complejo, useContext, useRef, memoización con useMemo y useCallback.

Intermedio Funciones Web
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Hooks Avanzados: useReducer, useContext, useRef, useMemo y useCallback

Más allá de useState y useEffect, React ofrece hooks que resuelven patrones más complejos: estado con lógica sofisticada, valores compartidos, referencias al DOM y optimización de rendimiento.


useReducer: estado complejo con acciones

useReducer es una alternativa a useState para gestionar estado con lógica de actualización compleja o múltiples sub-valores relacionados:

import { useReducer } from 'react'

interface State {
  count: number
  step: number
}

type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset' }
  | { type: 'setStep'; payload: number }

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step }
    case 'decrement':
      return { ...state, count: state.count - state.step }
    case 'reset':
      return { count: 0, step: 1 }
    case 'setStep':
      return { ...state, step: action.payload }
    default:
      return state
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 })

  return (
    <div>
      <p>Contador: {state.count} (step: {state.step})</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
      <input
        type="number"
        value={state.step}
        onChange={e => dispatch({ type: 'setStep', payload: Number(e.target.value) })}
      />
    </div>
  )
}

¿useState o useReducer?

Criterio useState useReducer
Estado simple (boolean, string, number) Overkill
Múltiples sub-valores relacionados Incómodo
Lógica de actualización compleja
Siguiente estado depende del anterior Ambos ✅ más claro
Testing de la lógica de estado Difícil ✅ Reducer es función pura

useContext: compartir datos sin prop drilling

Prop drilling es pasar props a través de muchos niveles de componentes que no los usan, solo para que lleguen al componente que los necesita.

Crear un Context

import { createContext, useContext, useState } from 'react'

// 1. Crear el contexto con tipo
interface ThemeContextType {
  theme: 'light' | 'dark'
  toggleTheme: () => void
}

const ThemeContext = createContext<ThemeContextType | null>(null)

// 2. Crear el Provider
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light')

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light')
  }

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

// 3. Custom hook para consumir (recomendado)
function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme debe usarse dentro de ThemeProvider')
  }
  return context
}

Usar el Context

// En App.tsx: envolver con el Provider
function App() {
  return (
    <ThemeProvider>
      <Header />
      <MainContent />
    </ThemeProvider>
  )
}

// En cualquier componente hijo (sin importar la profundidad):
function Header() {
  const { theme, toggleTheme } = useTheme()

  return (
    <header className={`header-${theme}`}>
      <button onClick={toggleTheme}>
        {theme === 'light' ? '🌙' : '☀️'}
      </button>
    </header>
  )
}

Context + useReducer (patrón escalable)

interface AuthState {
  user: User | null
  isAuthenticated: boolean
  loading: boolean
}

type AuthAction =
  | { type: 'LOGIN_SUCCESS'; payload: User }
  | { type: 'LOGOUT' }
  | { type: 'SET_LOADING'; payload: boolean }

function authReducer(state: AuthState, action: AuthAction): AuthState {
  switch (action.type) {
    case 'LOGIN_SUCCESS':
      return { user: action.payload, isAuthenticated: true, loading: false }
    case 'LOGOUT':
      return { user: null, isAuthenticated: false, loading: false }
    case 'SET_LOADING':
      return { ...state, loading: action.payload }
    default:
      return state
  }
}

const AuthContext = createContext<{
  state: AuthState
  dispatch: React.Dispatch<AuthAction>
} | null>(null)

function AuthProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(authReducer, {
    user: null, isAuthenticated: false, loading: true,
  })

  return (
    <AuthContext.Provider value={{ state, dispatch }}>
      {children}
    </AuthContext.Provider>
  )
}

useRef: referencias sin re-render

useRef crea una referencia mutable que persiste entre renders sin causar re-renderizado al cambiar:

Acceder al DOM

import { useRef, useEffect } from 'react'

function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    inputRef.current?.focus() // Foco automático al montar
  }, [])

  return <input ref={inputRef} placeholder="Escribe aquí..." />
}

Guardar valores mutables (sin re-render)

function StopWatch() {
  const [seconds, setSeconds] = useState(0)
  const intervalRef = useRef<NodeJS.Timeout | null>(null)

  const start = () => {
    if (intervalRef.current) return // Ya corriendo
    intervalRef.current = setInterval(() => {
      setSeconds(prev => prev + 1)
    }, 1000)
  }

  const stop = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current)
      intervalRef.current = null
    }
  }

  useEffect(() => {
    return () => stop() // Cleanup al desmontar
  }, [])

  return (
    <div>
      <p>{seconds}s</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  )
}

Guardar valor previo

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T | undefined>(undefined)

  useEffect(() => {
    ref.current = value
  }, [value])

  return ref.current
}

// Uso:
function Counter() {
  const [count, setCount] = useState(0)
  const prevCount = usePrevious(count)

  return (
    <p>
      Ahora: {count}, antes: {prevCount ?? 'N/A'}
    </p>
  )
}

useMemo: cachear cálculos costosos

useMemo memoriza el resultado de un cálculo, solo recalculando cuando cambian sus dependencias:

import { useMemo, useState } from 'react'

function ProductList({ products }: { products: Product[] }) {
  const [filter, setFilter] = useState('')
  const [sortBy, setSortBy] = useState<'name' | 'price'>('name')

  // ✅ Solo recalcula si products, filter o sortBy cambian
  const filteredAndSorted = useMemo(() => {
    console.log('Recalculando lista...')
    return products
      .filter(p => p.name.toLowerCase().includes(filter.toLowerCase()))
      .sort((a, b) => sortBy === 'name'
        ? a.name.localeCompare(b.name)
        : a.price - b.price
      )
  }, [products, filter, sortBy])

  return (
    <div>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      <ul>
        {filteredAndSorted.map(p => (
          <li key={p.id}>{p.name} — ${p.price}</li>
        ))}
      </ul>
    </div>
  )
}

¿Cuándo usarlo?

  • ✅ Cálculos costosos (filtrar/ordenar arrays grandes, cómputos matemáticos)
  • ✅ Evitar re-crear objetos/arrays que se pasan como props
  • No lo uses en todo: el overhead de useMemo puede ser mayor que el cálculo

useCallback: cachear funciones

useCallback memoriza la referencia de una función:

import { useCallback, useState, memo } from 'react'

// Componente hijo envuelto en memo (solo re-renderiza si sus props cambian)
const ExpensiveButton = memo(({ onClick, label }: {
  onClick: () => void
  label: string
}) => {
  console.log(`Renderizando botón: ${label}`)
  return <button onClick={onClick}>{label}</button>
})

function Parent() {
  const [count, setCount] = useState(0)
  const [text, setText] = useState('')

  // ❌ Sin useCallback: nueva función en cada render → ExpensiveButton re-renderiza
  // const increment = () => setCount(c => c + 1)

  // ✅ Con useCallback: misma referencia entre renders → ExpensiveButton NO re-renderiza
  const increment = useCallback(() => {
    setCount(c => c + 1)
  }, [])

  return (
    <div>
      <p>Count: {count}</p>
      <input value={text} onChange={e => setText(e.target.value)} />
      <ExpensiveButton onClick={increment} label="Incrementar" />
    </div>
  )
}

useMemo vs useCallback

// Son equivalentes:
const memoizedValue = useMemo(() => computeExpensive(a, b), [a, b])
const memoizedFn = useCallback((x) => doSomething(x, a), [a])

// useCallback(fn, deps) es azúcar para useMemo(() => fn, deps)

Resumen de hooks

Hook Propósito Causa re-render
useState Estado simple ✅ Sí
useReducer Estado complejo con acciones ✅ Sí
useEffect Efectos secundarios No (pero puede cambiar estado)
useContext Consumir Context ✅ Sí (cuando el valor cambia)
useRef Referencias mutables / DOM ❌ No
useMemo Cachear resultado de cálculo ❌ No
useCallback Cachear referencia de función ❌ No
🔒

Ejercicio práctico disponible

Implementa useReducer y un sistema de Context

Desbloquear ejercicios
// Implementa useReducer y un sistema de Context
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes