DEV Community

Pedro Alvarado
Pedro Alvarado

Posted on

🔹 2. `useEffect` – Efectos Secundarios y Ciclo de Vida

¿Qué es y para qué sirve?

useEffect conecta tus componentes con sistemas externos. Permite ejecutar "efectos secundarios" después de que el componente se haya renderizado. Un efecto secundario es cualquier código que afecta a algo fuera del propio componente, como:

  • Llamadas a una API para buscar datos.
  • Suscripciones a eventos (ej. window.addEventListener).
  • Manipulación manual del DOM.
  • Timers como setInterval o setTimeout.

useEffect reemplaza a los métodos de ciclo de vida componentDidMount, componentDidUpdate y componentWillUnmount de los componentes de clase.

Sintaxis y el array de dependencias

useEffect(() => {
  // ...tu código del efecto...

  return () => {
    // ...función de limpieza (opcional)...
  };
}, [dependencias]);
Enter fullscreen mode Exit fullscreen mode

La parte más importante de useEffect es el array de dependencias. Controla cuándo se ejecuta el efecto:

  1. [dependencia1, dependencia2]: El efecto se ejecuta solo si alguna de las dependencias ha cambiado desde el último render. Este es el caso más común.
  2. [] (array vacío): El efecto se ejecuta una sola vez, justo después del renderizado inicial (equivalente a componentDidMount). Perfecto para inicializaciones o fetching de datos que no cambian.
  3. No se proporciona el array: El efecto se ejecuta después de cada renderizado. ¡Cuidado! Esto puede causar bucles infinitos si el efecto actualiza el estado.

La función de limpieza (return) es opcional y se ejecuta:

  • Antes de que el efecto se vuelva a ejecutar (si una dependencia cambió).
  • Cuando el componente se desmonta (se elimina de la pantalla).

Es crucial para limpiar suscripciones, timers o listeners y evitar fugas de memoria.


Ejemplo práctico detallado: fetching de datos con estado de carga y error

Este es el caso de uso más común para useEffect. Vamos a buscar datos de un Pokémon y a manejar los estados intermedios.

import React, { useState, useEffect } from 'react';

function PokemonInfo({ pokemonName }) {
  const [pokemon, setPokemon] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!pokemonName) return;

    // Reseteamos los estados al empezar una nueva búsqueda
    setLoading(true);
    setError(null);
    setPokemon(null);

    fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonName}`)
      .then(response => {
        if (!response.ok) {
          throw new Error('No se encontró el Pokémon');
        }
        return response.json();
      })
      .then(data => {
        setPokemon(data);
      })
      .catch(error => {
        setError(error.message);
      })
      .finally(() => {
        setLoading(false);
      });

  }, [pokemonName]); // El efecto depende del nombre del Pokémon

  if (loading) return <p>Cargando...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <div>
      {pokemon && (
        <>
          <h3>{pokemon.name}</h3>
          <img src={pokemon.sprites.front_default} alt={pokemon.name} />
        </>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

✅ Buenas practicas y patrones comunes

  1. Un efecto, una responsabilidad: Si tienes lógicas no relacionadas, sepáralas en varios useEffect. Uno para fetching, otro para un event listener, etc. Esto hace el código más limpio.
  2. Limpia siempre tus efectos: Si te suscribes a algo, crea un timer o un listener, siempre devuelve una función de limpieza para cancelarlo.
  3. No olvides las dependencias: El linter de React te suele avisar si usas una variable o función dentro del efecto pero no la incluyes en el array. ¡No lo ignores! Omitir dependencias puede causar bugs muy difíciles de rastrear.
  4. Funciones en el array de dependencias: Si una función se usa dentro de useEffect, debe estar en el array. Pero si esa función se redefine en cada render, causará que el efecto se ejecute siempre. La solución es definir la función dentro del useEffect o memorizarla con useCallback.

🚨 Errores comunes y cómo evitarlos

  • Error: El bucle infinito.

    // MAL ❌
    const [count, setCount] = useState(0);
    useEffect(() => {
      // Este efecto cambia el estado, lo que causa un re-render,
      // lo que vuelve a ejecutar el efecto, y así hasta el infinito.
      setCount(count + 1);
    }); // Sin array de dependencias
    
    • Solución: Añade un array de dependencias. Si quieres que se ejecute solo una vez, que sea []. Si quieres que reaccione a un cambio, incluye la dependencia [otraVariable].
  • Error: Datos "viejos" (stale state) por dependencias faltantes.

    // MAL ❌
    const [count, setCount] = useState(0);
    useEffect(() => {
      // Este intervalo siempre "verá" count como 0, porque la función
      // del intervalo se creó en el primer render y nunca se actualizó.
      const intervalId = setInterval(() => {
        console.log(`El contador es: ${count}`);
      }, 1000);
      return () => clearInterval(intervalId);
    }, []); // El array vacío le dice a React: "nunca vuelvas a ejecutar esto"
    
    • Solución 1: Añadir la dependencia.

      // BIEN ✅ (pero recrea el intervalo cada vez que count cambia)
      useEffect(() => { ... }, [count]);
      
    • Solución 2: Usar la actualización funcional (mejor en este caso).

      // EXCELENTE ✅
      // Dentro de `setCount`, React siempre te da el valor más reciente.
      // No necesitamos `count` como dependencia.
      useEffect(() => {
        const intervalId = setInterval(() => {
           setCount(c => c + 1); // usamos la versión más reciente del estado
        }, 1000);
        return () => clearInterval(intervalId);
      }, []);
      

🚀 Retos prácticos

  1. Temporizador con Start/Stop/Reset: Usa useEffect y useState para crear un cronómetro funcional.
  2. Seguir el ratón: Crea un componente que muestre las coordenadas X e Y del ratón en tiempo real. (Pista: window.addEventListener('mousemove', ...), y no olvides limpiar el listener).
  3. Título de la página dinámico: Haz que el título del documento (document.title) se actualice para mostrar el valor de un contador o un input.
  4. Autoguardado: Crea un textarea que guarde su contenido en localStorage automáticamente 2 segundos después de que el usuario deje de escribir.

🔹 3. useContext – Compartir estado sin props

¿Qué es y para qué sirve?

useContext resuelve un problema muy común en React llamado "prop drilling". Esto ocurre cuando tienes que pasar props a través de múltiples niveles de componentes anidados, incluso si los componentes intermedios no las necesitan.

useContext te permite crear un "almacén" de datos global o semi-global al que cualquier componente dentro de un árbol específico puede acceder directamente, sin necesidad de recibirlo por props.

Úsalo para:

  • Gestionar el tema de la aplicación (oscuro/claro).
  • Información del usuario autenticado.
  • Configuraciones de idioma.
  • Estado de un carrito de compras.

Sintaxis y flujo de trabajo

El uso de Context API se divide en 3 pasos:

  1. Crear el Contexto:

    // theme-context.js
    import { createContext } from 'react';
    // El valor 'light' es el valor por defecto, solo se usa si un
    // componente intenta consumir el contexto sin un Provider por encima.
    export const ThemeContext = createContext('light');
    
  2. Proveer el Contexto:
    En algún lugar arriba en tu árbol de componentes (como App.js), envuelve a los componentes hijos con el Provider del contexto y pásale un value.

    // App.js
    import { ThemeContext } from './theme-context';
    
    function App() {
      const [theme, setTheme] = useState('dark');
      // Todos los componentes dentro de este Provider podrán acceder al valor 'dark'.
      return (
        <ThemeContext.Provider value={{ theme, setTheme }}>
          <Layout />
        </ThemeContext.Provider>
      );
    }
    
  3. Consumir el Contexto:
    Cualquier componente hijo puede usar el hook useContext para leer (y en este caso, modificar) el valor.

    // Button.js
    import { useContext } from 'react';
    import { ThemeContext } from './theme-context';
    
    function Button() {
      const { theme, setTheme } = useContext(ThemeContext);
      const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');
    
      return <button onClick={toggleTheme}>Cambiar a tema {theme === 'light' ? 'oscuro' : 'claro'}</button>;
    }
    

Ejemplo práctico detallado: proveedor de autenticación

Vamos a crear un sistema simple para manejar el estado de autenticación de un usuario en toda la app.

1. Crear el AuthContext.js

// contexts/AuthContext.js
import React, { createContext, useState, useContext } from 'react';

// 1. Crear el contexto
const AuthContext = createContext(null);

// 2. Crear el Proveedor (un componente personalizado)
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null); // El usuario no está logueado por defecto

  const login = (username) => setUser({ name: username });
  const logout = () => setUser(null);

  // Pasamos tanto el estado como las funciones para modificarlo
  const value = { user, login, logout };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// 3. Crear un Custom Hook para consumir el contexto (buena práctica)
export function useAuth() {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error('useAuth debe ser usado dentro de un AuthProvider');
  }
  return context;
}
Enter fullscreen mode Exit fullscreen mode

2. Envolver la App con AuthProvider

// App.js
import { AuthProvider } from './contexts/AuthContext';
import Navbar from './components/Navbar';

function App() {
  return (
    <AuthProvider>
      <h1>Mi Aplicación</h1>
      <Navbar />
      {/* ...el resto de la app... */}
    </AuthProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

3. Usar el contexto en cualquier componente

// components/Navbar.js
import { useAuth } from '../contexts/AuthContext';

function Navbar() {
  const { user, login, logout } = useAuth(); // ¡Así de fácil!

  return (
    <nav>
      {user ? (
        <>
          <span>Bienvenido, {user.name}</span>
          <button onClick={logout}>Cerrar Sesión</button>
        </>
      ) : (
        <button onClick={() => login('UsuarioPrueba')}>Iniciar Sesión</button>
      )}
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

✅ Buenas practicas y patrones comunes

  1. Crear un Custom Provider: Como en el ejemplo anterior, es una excelente práctica crear un componente MiProveedor que encapsule la lógica del estado (useState o useReducer) junto con el Context.Provider. Esto mantiene la lógica centralizada y el componente App más limpio.
  2. Crear un Custom Hook consumidor: useMiContexto (como useAuth) hace que el consumo sea más declarativo y permite añadir validaciones, como comprobar si el hook se está usando dentro del proveedor correcto.
  3. Separar contextos: No pongas todo el estado de tu aplicación en un solo contexto gigante. Divídelo por dominios: AuthContext, ThemeContext, CartContext, etc. Esto evita re-renderizados innecesarios.
  4. Optimización de value: Cada vez que el componente que contiene al Provider se re-renderiza, se crea un nuevo objeto para value. Si este objeto contiene funciones, créalas con useCallback para evitar re-renderizados innecesarios en los componentes consumidores.

🚨 Errores comunes y cómo evitarlos

  • Error: Olvidar envolver los componentes con el <MiContext.Provider>.
    • Solución: Asegúrate de que el Provider esté en un nivel superior en el árbol de componentes. El custom hook con la validación (como en el ejemplo de useAuth) te ayudará a detectar este error rápidamente.
  • Error: Re-renderizados excesivos.

    // PROBLEMA 😬
    function App() {
      const [count, setCount] = useState(0);
      // El objeto `value` se crea de nuevo en CADA render de App,
      // incluso si `count` es el que cambia. Esto hace que TODOS los
      // consumidores de ThemeContext se re-rendericen.
      return (
        <ThemeContext.Provider value={{ theme: 'dark' }}>
           <button onClick={() => setCount(c => c + 1)}>{count}</button>
           <MiComponentePesado />
        </ThemeContext.Provider>
      );
    }
    
    • Solución: Saca el value del flujo del render si es estático, o memorízalo con useMemo si depende de props o estado.
    // BIEN ✅
    const themeValue = useMemo(() => ({ theme: 'dark' }), []);
    // O mejor aún, si es totalmente estático:
    const themeValue = { theme: 'dark' };
    return <ThemeContext.Provider value={themeValue}>...</ThemeContext.Provider>;
    

🚀 Retos prácticos

  1. Selector de Idioma: Crea un LanguageContext que permita a los componentes mostrar texto en "Español" o "Inglés". Unos botones en la Navbar deberían poder cambiar el idioma de toda la aplicación.
  2. Notificaciones Globales: Implementa un NotificationContext que permita a cualquier componente mostrar un mensaje de notificación (ej. "Producto añadido al carrito"). Debe tener una función showNotification(message).
  3. Carrito de Compras con useReducer y useContext: Combina ambos hooks. Crea un CartContext que use un useReducer internamente para manejar la lógica de añadir, eliminar y actualizar productos en el carrito.

🔹 4. useReducer – Manejar estados complejos

¿Qué es y para qué sirve?

useReducer es una alternativa a useState para gestionar lógicas de estado más complejas. Si te encuentras con un useState que maneja un objeto grande con muchas funciones de actualización diferentes y que se están volviendo difíciles de manejar, useReducer es la solución.

Centraliza toda la lógica de actualización del estado en una única función llamada "reducer", haciendo que el flujo de datos sea más predecible y fácil de depurar. Es conceptualmente similar a Redux, pero integrado en React.

Úsalo cuando:

  • El próximo estado depende de una lógica compleja basada en el estado anterior.
  • El estado tiene una estructura compleja (objetos anidados, arrays de objetos).
  • La lógica de actualización del estado necesita ser probada de forma aislada.
  • Quieres optimizar el rendimiento de componentes que disparan actualizaciones profundas, ya que dispatch no cambia entre renders.

Sintaxis y flujo de trabajo

const [state, dispatch] = useReducer(reducer, initialState);
Enter fullscreen mode Exit fullscreen mode
  • reducer: Una función pura (state, action) => newState que toma el estado actual y una "acción", y devuelve el nuevo estado.
  • initialState: El estado inicial, similar al de useState.
  • state: El estado actual.
  • dispatch: Una función que "despacha" acciones a la función reducer. Es la única forma de desencadenar una actualización de estado. Una acción es típicamente un objeto con una propiedad type (ej. { type: 'INCREMENT' }).

El flujo es: Componente → dispatch(action)reducer(state, action) → Nuevo Estado → Re-renderizado.


Ejemplo práctico detallado: lista de tareas (Todo List)

Este es el ejemplo canónico para useReducer, ya que implica múltiples tipos de actualizaciones sobre un array de objetos.

1. Definir el Reducer y el Estado Inicial

// components/TodoList.js

// Estado inicial: un array de tareas
const initialState = {
  todos: [
    { id: 1, text: 'Aprender useReducer', completed: true },
    { id: 2, text: 'Practicar con un ejemplo', completed: false },
  ],
};

// Acciones que podemos despachar
export const ACTIONS = {
  ADD_TODO: 'add-todo',
  TOGGLE_TODO: 'toggle-todo',
  DELETE_TODO: 'delete-todo',
};

// El reducer contiene toda la lógica de actualización
function reducer(state, action) {
  switch (action.type) {
    case ACTIONS.ADD_TODO:
      return {
        ...state,
        todos: [...state.todos, { id: Date.now(), text: action.payload.text, completed: false }],
      };
    case ACTIONS.TOGGLE_TODO:
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo
        ),
      };
    case ACTIONS.DELETE_TODO:
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload.id),
      };
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Usar useReducer en el Componente

import React, { useReducer, useState } from 'react';
// (el código del reducer y el estado inicial va aquí arriba)

function TodoList() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const [newTodoText, setNewTodoText] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (newTodoText.trim() === '') return;
    dispatch({ type: ACTIONS.ADD_TODO, payload: { text: newTodoText } });
    setNewTodoText('');
  };

  return (
    <div>
      <h3>Lista de Tareas con useReducer</h3>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={newTodoText}
          onChange={(e) => setNewTodoText(e.target.value)}
          placeholder="Añadir nueva tarea"
        />
        <button type="submit">Añadir</button>
      </form>
      <ul>
        {state.todos.map(todo => (
          <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.text}
            <button onClick={() => dispatch({ type: ACTIONS.TOGGLE_TODO, payload: { id: todo.id } })}>
              {todo.completed ? 'Undo' : 'Completar'}
            </button>
            <button onClick={() => dispatch({ type: ACTIONS.DELETE_TODO, payload: { id: todo.id } })}>
              Eliminar
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

✅ Buenas practicas y patrones comunes

  1. Reducers Puros: Un reducer nunca debe modificar el estado original (state) o la acción (action). Siempre debe devolver un nuevo objeto de estado. Tampoco debe realizar efectos secundarios (como llamadas a API).
  2. Usar payload para los datos: Es una convención común pasar los datos necesarios para la actualización dentro de una propiedad payload en el objeto de la acción. Ej: { type: 'ADD', payload: { newItem } }.
  3. Exportar constantes de acción: Definir los type de las acciones como constantes (como en el ejemplo ACTIONS) ayuda a evitar errores de tipeo y facilita el mantenimiento.
  4. Separar el reducer: Para componentes muy complejos o para reutilizar la lógica, puedes definir el reducer y el estado inicial en un archivo separado e importarlos.

🚨 Errores comunes y cómo evitarlos

  • Error: Mutar el estado en el reducer.

    // MAL ❌
    case ACTIONS.DELETE_TODO:
      // Esto modifica el array original, es un anti-patrón.
      const index = state.todos.findIndex(t => t.id === action.payload.id);
      state.todos.splice(index, 1);
      return state;
    
    • Solución: Siempre retorna una copia nueva del estado.
    // BIEN ✅
    case ACTIONS.DELETE_TODO:
      return { ...state, todos: state.todos.filter(t => t.id !== action.payload.id) };
    
  • Error: Olvidar el default case en el switch.

    • Solución: Si tu reducer recibe una acción que no reconoce, debería devolver el estado actual sin modificarlo. El default: return state; se encarga de esto. Sin él, tu reducer devolvería undefined y tu aplicación se rompería.

🚀 Retos prácticos

  1. Formulario Complejo: Convierte un formulario con varios campos (useState con un objeto) a un useReducer. Crea acciones para UPDATE_FIELD y RESET_FORM.
  2. Carrito de Compras: Gestiona un carrito de compras con acciones como ADD_ITEM, REMOVE_ITEM, INCREMENT_QUANTITY, DECREMENT_QUANTITY y CLEAR_CART.
  3. Juego Simple: Crea un juego simple (como un "clicker" con mejoras que puedes comprar) donde el estado del juego (puntos, mejoras, etc.) se gestione con useReducer.

🔹 5. useRef – Referencias y valores persistentes

¿Qué es y para qué sirve?

useRef es un hook versátil con dos casos de uso principales que, aunque parezcan diferentes, se basan en el mismo principio: proporcionar un objeto mutable (.current) que persiste durante todo el ciclo de vida del componente sin causar re-renderizados cuando cambia.

  1. Acceder a elementos del DOM: Es la "salida de emergencia" para interactuar directamente con un elemento del DOM, por ejemplo, para manejar el foco, medir su tamaño o integrar librerías de terceros (como D3.js).
  2. Guardar "variables de instancia": Te permite mantener un valor mutable que no necesita disparar una nueva renderización. Es como tener una variable de instancia en un componente de clase. Perfecto para guardar IDs de timers, contadores internos, o cualquier valor que necesites que sobreviva entre renders.

Sintaxis y características clave

const miRef = useRef(valorInicial);
Enter fullscreen mode Exit fullscreen mode
  • useRef devuelve un objeto con una única propiedad: current.
  • miRef.current se inicializa con valorInicial.
  • Puedes cambiar miRef.current directamente: miRef.current = nuevoValor.
  • Importante: Cambiar miRef.current NO dispara un re-renderizado.

Ejemplo práctico detallado: input con foco y contador de renders

Este ejemplo muestra ambos usos de useRef: uno para el DOM y otro para una variable persistente.

import React, { useState, useEffect, useRef } from 'react';

function FocusCounter() {
  const [inputValue, setInputValue] = useState('');

  // Caso de uso 1: Referencia al DOM
  const inputRef = useRef(null);

  // Caso de uso 2: Variable persistente
  const renderCount = useRef(0);

  useEffect(() => {
    // Se ejecuta después de cada render
    renderCount.current = renderCount.current + 1;
  });

  const handleFocus = () => {
    // Accedemos al elemento del DOM directamente
    inputRef.current.focus();
  };

  return (
    <div>
      <h3>Ejemplo de useRef</h3>
      <input
        ref={inputRef} // Conectamos la ref al elemento input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="Escribe algo..."
      />
      <button onClick={handleFocus}>Poner Foco en el Input</button>

      <p>El valor del input es: "{inputValue}"</p>
      <p>Este componente se ha renderizado {renderCount.current} veces.</p>
      <p>(Nota: el contador de renders no causa re-renders por sí mismo)</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

En este ejemplo:

  • inputRef nos da acceso directo al <input>, permitiéndonos llamar a .focus().
  • renderCount lleva la cuenta de las renderizaciones. Si usáramos useState para esto, cada actualización del contador causaría otro render, creando un bucle infinito. Con useRef, podemos actualizar el valor sin efectos secundarios.

✅ Buenas practicas y patrones comunes

  1. Cuándo usar useRef vs useState:
    • Usa useState si el cambio en el valor debe ser visible en la UI.
    • Usa useRef si necesitas que un valor persista entre renders pero no quieres que su cambio dispare una nueva renderización.
  2. Guardar el valor previo de props o estado: Un patrón muy útil es combinar useRef y useEffect para "recordar" un valor del render anterior.

    function Componente({ valor }) {
      const valorPrevioRef = useRef();
      useEffect(() => {
        valorPrevioRef.current = valor;
      }, [valor]); // Se actualiza después de que el render se completa
      const valorPrevio = valorPrevioRef.current;
      // ...
    }
    
  3. Manejo de Timers: useRef es ideal para guardar el ID de setInterval o setTimeout para poder limpiarlo después.

🚨 Errores comunes y cómo evitarlos

  • Error: Acceder a ref.current demasiado pronto.

    // MAL ❌
    function MiComponente() {
      const miRef = useRef(null);
      // Aquí, durante el primer render, miRef.current es `null`
      // porque el DOM aún no existe. Esto dará un error.
      miRef.current.focus();
    
      return <input ref={miRef} />;
    }
    
    • Solución: Accede a la ref dentro de un useEffect (que se ejecuta después del renderizado) o en manejadores de eventos (que se ejecutan por interacción del usuario).
    // BIEN ✅
    useEffect(() => {
      // Para el primer render, esto se ejecuta después de que el input existe.
      miRef.current.focus();
    }, []); // Array vacío para que solo se ejecute una vez
    
  • Error: Mostrar el valor de una ref en la UI y esperar que se actualice.

    • Solución: Si necesitas que un cambio se refleje en el render, ese valor debe estar en el estado (useState). Recuerda: useRef no notifica a React cuando cambia.

🚀 Retos prácticos

  1. Reproductor de Video: Crea un componente que muestre un video (<video>). Usa useRef para obtener una referencia al elemento y añade botones para "Play", "Pause" y "Stop".
  2. Cronómetro Preciso: Haz un cronómetro que use setInterval. Guarda el ID del intervalo en una ref para poder detenerlo y reiniciarlo correctamente.
  3. Scroll a un Elemento: Crea una lista larga de elementos. Añade botones al principio que, al hacer clic, hagan scroll automáticamente a un elemento específico en medio de la lista (pista: element.scrollIntoView()).

🔹 6. useMemo y useCallback – Optimización de rendimiento

El problema: re-renderizados innecesarios y la igualdad referencial

Antes de entender la solución, es crucial entender el problema. En JavaScript, los objetos y las funciones son iguales por referencia, no por valor.

// A pesar de tener el mismo "valor", son dos objetos diferentes en memoria.
{} === {} // false

// Lo mismo ocurre con las funciones.
(() => {}) === (() => {}) // false
Enter fullscreen mode Exit fullscreen mode

En React, cada vez que un componente se re-renderiza, las funciones y objetos declarados dentro de él se crean de nuevo. Esto significa que sus referencias cambian en cada render.

¿Por qué es un problema? Si pasas una de estas funciones u objetos como prop a un componente hijo optimizado con React.memo, el hijo se re-renderizará de todas formas, porque desde su perspectiva, la prop (onSomething) ha cambiado.

Aquí es donde entran useMemo y useCallback. Son herramientas para decir a React: "No vuelvas a crear esto a menos que sus dependencias cambien".


useMemo: Memorizando valores calculados

useMemo "memoriza" (cachea) el resultado de un cálculo costoso. La función que le pasas solo se volverá a ejecutar si una de las dependencias ha cambiado.

Úsalo para:

  • Cálculos computacionalmente caros (ej. filtrar, ordenar o transformar grandes listas).
  • Evitar que se creen nuevos objetos/arrays en cada render si se pasan como props a componentes optimizados.

Sintaxis:

const valorMemorizado = useMemo(() => calcularValorCostoso(a, b), [a, b]);
Enter fullscreen mode Exit fullscreen mode

useCallback: Memorizando funciones

useCallback es muy similar, pero en lugar de memorizar el resultado de una función, memoriza la propia definición de la función.

Úsalo para:

  • Evitar re-renderizados en componentes hijos optimizados (React.memo) que reciben funciones como props.
  • Como dependencia estable en otros hooks como useEffect.

Sintaxis:

const funcionMemorizada = useCallback(() => {
  hacerAlgo(a, b);
}, [a, b]);
Enter fullscreen mode Exit fullscreen mode

Es conceptualmente equivalente a: useMemo(() => () => { hacerAlgo(a, b); }, [a, b]).


Ejemplo práctico detallado: lista filtrable optimizada

Imagina una lista de usuarios que puedes filtrar. El filtrado puede ser lento si la lista es grande. Además, cada item de la lista tiene un botón para eliminarlo.

Componente UserItem (Optimizado)

import React from 'react';

// React.memo evita que este componente se re-renderice si sus props no cambian.
const UserItem = React.memo(({ user, onDelete }) => {
  console.log(`Renderizando UserItem para: ${user.name}`);
  return (
    <li>
      {user.name}
      <button onClick={() => onDelete(user.id)}>Eliminar</button>
    </li>
  );
});
Enter fullscreen mode Exit fullscreen mode

Componente Principal UserList

import React, { useState, useMemo, useCallback } from 'react';

const initialUsers = [
  { id: 1, name: 'Ana' },
  { id: 2, name: 'Carlos' },
  { id: 3, name: 'Beatriz' },
  { id: 4, name: 'Daniel' },
];

function UserList() {
  const [users, setUsers] = useState(initialUsers);
  const [filter, setFilter] = useState('');
  const [theme, setTheme] = useState('light'); // Estado extra para forzar re-renders

  // 1. Optimizando el cálculo del filtrado con `useMemo`
  // Esta función solo se re-ejecutará si `users` o `filter` cambian,
  // no cuando `theme` cambie.
  const filteredUsers = useMemo(() => {
    console.log('Filtrando usuarios...');
    return users.filter(user =>
      user.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [users, filter]);

  // 2. Optimizando la función de borrado con `useCallback`
  // `handleDelete` mantiene la misma referencia mientras `users` no cambie.
  // Esto evita que todos los `UserItem` se re-rendericen cuando cambiamos el tema o el filtro.
  const handleDelete = useCallback((userId) => {
    setUsers(prevUsers => prevUsers.filter(user => user.id !== userId));
  }, []); // Dependencia vacía porque usamos la actualización funcional de `setUsers`

  return (
    <div style={{ background: theme === 'light' ? 'white' : 'black', color: theme === 'light' ? 'black' : 'white' }}>
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        Cambiar Tema
      </button>
      <hr />
      <input
        type="text"
        placeholder="Filtrar usuarios..."
        value={filter}
        onChange={e => setFilter(e.target.value)}
      />
      <ul>
        {filteredUsers.map(user => (
          <UserItem key={user.id} user={user} onDelete={handleDelete} />
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

✅ Buenas practicas y patrones comunes

  1. NO optimices prematuramente: useMemo y useCallback tienen un coste. Usarlos en todos lados puede hacer tu aplicación más lenta. Aplícalos solo cuando hayas identificado un problema de rendimiento real (puedes usar el Profiler de React DevTools).
  2. React.memo es tu mejor amigo: useCallback por sí solo no hace nada si el componente que recibe la función no está envuelto en React.memo (o PureComponent).
  3. Dependencias, dependencias, dependencias: El array de dependencias es la clave. Asegúrate de incluir todos los valores del scope del componente que se usan dentro de la función memorizada. El linter de React te ayudará con esto.

🚨 Errores comunes y cómo evitarlos

  • Error: Usar useMemo para memorizar componentes.
    • Solución: Para memorizar un componente, envuélvelo en React.memo. useMemo es para valores, no para JSX.
  • Error: Olvidar el array de dependencias.
    • Solución: Si omites el array, la función se recalculará en cada render, haciendo la optimización inútil. Si quieres que se calcule solo una vez, usa [].
  • Error: Pasar una función creada en el cuerpo del componente a las dependencias de useEffect.

    // MAL ❌
    function MiComponente() {
      const fetchData = () => { /* ... */ };
      useEffect(() => {
        fetchData();
      }, [fetchData]); // `fetchData` es nueva en cada render -> bucle infinito
    }
    
    • Solución: Define la función dentro del useEffect si solo se usa ahí, o envuélvela en useCallback si se usa en varios sitios.
    // BIEN ✅
    const fetchData = useCallback(() => { /* ... */ }, []);
    useEffect(() => {
      fetchData();
    }, [fetchData]);
    

🚀 Retos prácticos

  1. Cálculo de Factorial: Crea un componente con un input numérico. Calcula y muestra el factorial de ese número. El cálculo debe estar memorizado con useMemo. Añade otro estado (como un toggle de tema) para verificar que el factorial no se recalcula innecesariamente.
  2. Lista de Hijos con React.memo: Crea un componente padre que renderiza una lista de 100 componentes hijos. Pasa una función onPress al hijo. Comprueba con un console.log cuántos hijos se re-renderizan cuando el padre cambia su estado. Luego, optimiza con useCallback y React.memo y comprueba la diferencia.
  3. Gráfico SVG Complejo: Simula un componente que renderiza un gráfico SVG (que puede ser costoso). Las coordenadas del gráfico se calculan a partir de un array de datos. Usa useMemo para memorizar las coordenadas calculadas y evitar re-calcularlas si otras props del componente cambian.

🔹 7. Custom Hooks – Reusar lógica

¿Qué son y por qué son tan potentes?

Los Custom Hooks (Hooks Personalizados) son la característica más poderosa de los Hooks. Te permiten extraer y reutilizar lógica con estado de un componente. No son un hook nuevo de React, sino una convención: una función JavaScript que sigue dos reglas:

  1. Su nombre debe empezar con use (ej. useFetch, useLocalStorage).
  2. Puede llamar a otros hooks (como useState, useEffect, etc.).

Piensa en ellos como piezas de Lego lógicas. ¿Necesitas saber si el usuario está online? useOnlineStatus. ¿Quieres interactuar con el Local Storage? useLocalStorage. ¿Necesitas hacer fetching de datos en varios sitios? useFetch.

Creando nuestro primer Custom Hook: useFetch

El fetching de datos es un caso de uso perfecto. Casi siempre necesitas manejar el estado de carga, los posibles errores y los datos resultantes. Vamos a encapsular toda esa lógica en un hook reutilizable.

El código del hook useFetch.js:

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

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

  useEffect(() => {
    // Si la URL no es válida, no hacemos nada.
    if (!url) return;

    // Usamos AbortController para cancelar el fetch si el componente
    // se desmonta o la URL cambia antes de que termine.
    const controller = new AbortController();

    const fetchData = async () => {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) {
          throw new Error(`Error: ${response.status}`);
        }
        const result = await response.json();
        setData(result);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        // Solo cambiamos loading a false si no fue un aborto
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      }
    };

    fetchData();

    // Función de limpieza: aborta el fetch si es necesario.
    return () => {
      controller.abort();
    };
  }, [url]); // Se vuelve a ejecutar si la URL cambia

  // El hook devuelve un objeto con el estado del fetch
  return { data, loading, error };
}
Enter fullscreen mode Exit fullscreen mode

Usando useFetch en un componente

Ahora, consumir esta lógica compleja se vuelve trivialmente simple.

// components/GitHubUser.js
import React, { useState } from 'react';
import { useFetch } from '../hooks/useFetch';

function GitHubUser() {
  const [username, setUsername] = useState('facebook');
  const [input, setInput] = useState('facebook');

  // ¡Toda la lógica de fetch encapsulada en una sola línea!
  const { data, loading, error } = useFetch(`https://api.github.com/users/${username}`);

  const handleSubmit = (e) => {
    e.preventDefault();
    setUsername(input);
  }

  return (
    <div>
      <h3>Buscador de Usuarios de GitHub</h3>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Introduce un usuario de GitHub"
        />
        <button type="submit">Buscar</button>
      </form>

      {loading && <p>Cargando...</p>}
      {error && <p>Error: {error}</p>}
      {data && (
        <div>
          <h4>{data.name} (@{data.login})</h4>
          <img src={data.avatar_url} alt={data.name} width="100" />
          <p>{data.bio}</p>
        </div>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Como puedes ver, el componente GitHubUser no sabe nada sobre useState o useEffect. Solo le interesa el resultado final: data, loading y error. Esto hace que el código sea increíblemente limpio, declarativo y fácil de mantener.


✅ Buenas practicas y patrones comunes

  1. Nombres que empiezan con use: Es obligatorio. Esta convención permite a React y a las herramientas de linting saber que tu función es un hook y que debe seguir las reglas de los hooks.
  2. Devuelve un array o un objeto: Si tu hook devuelve múltiples valores, devuélvelos en un objeto { valor1, valor2 } para que el consumo sea más legible. Si es un hook muy genérico y que emula a uno nativo (como useState), puedes devolver un array [valor, actualizador].
  3. Hazlos genéricos y configurables: Un buen custom hook acepta parámetros para modificar su comportamiento. En useFetch, podría aceptar un objeto de opciones para el fetch.
  4. No rompas las reglas de los hooks: Dentro de tu custom hook, sigue aplicando las mismas reglas: no los llames en bucles, condicionales o funciones anidadas.

🚨 Errores comunes y cómo evitarlos

  • Error: Olvidar el prefijo use.
    • Solución: Nombra siempre tus hooks personalizados como useMiHook. Si no, React no podrá verificar que estás siguiendo las reglas de los hooks.
  • Error: Compartir estado entre componentes.
    • Aclaración: Es un error conceptual pensar que un custom hook comparte el mismo estado entre diferentes componentes que lo usan. Cada llamada a un custom hook es completamente independiente y tiene su propio estado interno. Si quieres compartir estado, necesitas useContext o una librería de estado global.

🚀 Retos prácticos

  1. useLocalStorage: Crea un hook que se sincronice con localStorage. Debe funcionar de manera similar a useState, pero persistiendo el valor en el navegador.

    // Uso esperado:
    const [name, setName] = useLocalStorage('username', 'Invitado');
    
  2. useOnlineStatus: Crea un hook que devuelva true si el navegador está conectado a internet y false si no. (Pista: window.addEventListener('online', ...) y window.addEventListener('offline', ...)).

  3. useDebounce: Un hook muy útil. Toma un valor (ej. el texto de un input) y devuelve una versión "retrasada" de ese valor que solo se actualiza después de un cierto tiempo sin cambios. Es perfecto para evitar hacer peticiones a una API en cada pulsación de tecla.

    // Uso esperado:
    const [searchTerm, setSearchTerm] = useState('');
    const debouncedSearchTerm = useDebounce(searchTerm, 500); // 500ms de retraso
    // Usa `debouncedSearchTerm` en tu `useFetch`
    

🔹 8. Errores comunes y soluciones

Esta sección resume los problemas más frecuentes que encontrarás al trabajar con Hooks, muchos de los cuales son la causa raíz de bugs difíciles de rastrear.

1. Romper las reglas de los hooks

React es estricto con estas dos reglas por una razón fundamental: React depende del orden en que se llaman los Hooks para asociar el estado y otros datos con la fibra del componente correcto.

  • ❌ No llames Hooks dentro de bucles, condicionales o funciones anidadas.
  • ❌ No llames Hooks desde funciones JavaScript normales. (Solo desde componentes de React o Custom Hooks).
// MAL ❌
if (condicion) {
  // El orden de llamada cambia, React se pierde.
  const [valor, setValor] = useState(0);
}
Enter fullscreen mode Exit fullscreen mode

Consecuencia: Errores crípticos, estado que se mezcla entre diferentes hooks, y comportamiento impredecible.
Solución: Llama siempre a los hooks en el nivel superior de tu componente. Si necesitas lógica condicional, ponla dentro del hook.

// BIEN ✅
useEffect(() => {
  if (condicion) {
    // La lógica condicional está *dentro* del hook.
    // El hook en sí se llama incondicionalmente.
  }
}, [condicion]);
Enter fullscreen mode Exit fullscreen mode

2. El estado "viejo" (Stale State) por closures

Este es el concepto más difícil pero más importante de entender. Cuando un componente se renderiza, "captura" los valores de las props y el estado de ese render específico en sus funciones (closures).

// PROBLEMA 😬
function Contador() {
  const [count, setCount] = useState(0);

  const mostrarAlerta = () => {
    // Esta función "recuerda" el valor de `count`
    // del render en que fue creada.
    alert('El contador es ' + count);
  };

  return (
    <div>
      <p>Contador: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={mostrarAlerta}>Mostrar Alerta</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Si haces clic en "+1" varias veces y luego en "Mostrar Alerta", la alerta mostrará el valor de count que existía cuando se renderizó el botón, no el valor actual.

Solución 1: La Actualización Funcional
Si el nuevo estado depende del anterior, usa la forma funcional de setState. React te garantiza que siempre recibirás la versión más reciente del estado.

// Ideal para eventos o intervalos
setCount(prevCount => prevCount + 1);
Enter fullscreen mode Exit fullscreen mode

Solución 2: El Array de Dependencias
Para useEffect y useCallback, asegúrate de que todas las variables externas que usas estén en el array de dependencias. Esto garantiza que la función se "refresque" con los nuevos valores cuando cambien.

3. El infame array de dependencias

  • Omitir dependencias: Causa "stale state" porque el hook usa una versión vieja de una función o variable. El linter de React (eslint-plugin-react-hooks) es tu mejor amigo para detectar esto. ¡No lo ignores!
  • Dependencias que cambian demasiado: Si incluyes un objeto o una función que se redefine en cada render, el efecto se ejecutará constantemente.
    • Solución: Para funciones, usa useCallback. Para objetos, usa useMemo. Para dependencias de setState de un useState, puedes omitirlas, ya que React garantiza que son estables.

4. Abuso de la optimización

No envuelvas todo en useMemo y useCallback por defecto.

  • Coste: Estos hooks no son gratuitos. Añaden complejidad y consumen memoria.
  • Cuándo usarlos:
    1. Cuando pasas props a un componente hijo envuelto en React.memo.
    2. Para cálculos realmente pesados que están ralentizando tu componente.
    3. Para estabilizar una dependencia de otro hook (como useEffect).

Regla de oro: No optimices hasta que midas. Usa el Profiler de las React DevTools para encontrar cuellos de botella reales.


🔹 9. Recursos adicionales

Para seguir profundizando, estos son los mejores lugares a los que puedes acudir.

  1. Documentación Oficial de React (en español)

  2. Artículos y Guías Visuales

  3. Herramientas

    • React DevTools Profiler: Una extensión para el navegador indispensable para encontrar problemas de rendimiento y entender qué está causando los re-renderizados.
    • ESLint Plugin for React Hooks: Viene incluido por defecto en Create React App. Te avisará de errores comunes, especialmente con los arrays de dependencias.

¡Feliz codeo! 🚀

Top comments (0)