¿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
osetTimeout
.
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]);
La parte más importante de useEffect
es el array de dependencias. Controla cuándo se ejecuta el efecto:
-
[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. -
[]
(array vacío): El efecto se ejecuta una sola vez, justo después del renderizado inicial (equivalente acomponentDidMount
). Perfecto para inicializaciones o fetching de datos que no cambian. - 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>
);
}
✅ Buenas practicas y patrones comunes
- Un efecto, una responsabilidad: Si tienes lógicas no relacionadas, sepáralas en varios
useEffect
. Uno para fetching, otro para unevent listener
, etc. Esto hace el código más limpio. - Limpia siempre tus efectos: Si te suscribes a algo, crea un timer o un listener, siempre devuelve una función de limpieza para cancelarlo.
- 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.
- 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 deluseEffect
o memorizarla conuseCallback
.
🚨 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]
.
- Solución: Añade un array de dependencias. Si quieres que se ejecute solo una vez, que sea
-
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
- Temporizador con Start/Stop/Reset: Usa
useEffect
yuseState
para crear un cronómetro funcional. - 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). - 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. - Autoguardado: Crea un
textarea
que guarde su contenido enlocalStorage
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:
-
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');
-
Proveer el Contexto:
En algún lugar arriba en tu árbol de componentes (comoApp.js
), envuelve a los componentes hijos con elProvider
del contexto y pásale unvalue
.
// 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> ); }
-
Consumir el Contexto:
Cualquier componente hijo puede usar el hookuseContext
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;
}
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>
);
}
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>
);
}
✅ Buenas practicas y patrones comunes
- 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
ouseReducer
) junto con elContext.Provider
. Esto mantiene la lógica centralizada y el componenteApp
más limpio. - Crear un Custom Hook consumidor:
useMiContexto
(comouseAuth
) 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. - 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. - Optimización de
value
: Cada vez que el componente que contiene alProvider
se re-renderiza, se crea un nuevo objeto paravalue
. Si este objeto contiene funciones, créalas conuseCallback
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 deuseAuth
) te ayudará a detectar este error rápidamente.
- Solución: Asegúrate de que el
-
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 conuseMemo
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>;
- Solución: Saca el
🚀 Retos prácticos
- Selector de Idioma: Crea un
LanguageContext
que permita a los componentes mostrar texto en "Español" o "Inglés". Unos botones en laNavbar
deberían poder cambiar el idioma de toda la aplicación. - 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ónshowNotification(message)
. - Carrito de Compras con
useReducer
yuseContext
: Combina ambos hooks. Crea unCartContext
que use unuseReducer
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);
-
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 deuseState
. -
state
: El estado actual. -
dispatch
: Una función que "despacha" acciones a la funciónreducer
. Es la única forma de desencadenar una actualización de estado. Una acción es típicamente un objeto con una propiedadtype
(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;
}
}
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>
);
}
✅ Buenas practicas y patrones comunes
- 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). - Usar
payload
para los datos: Es una convención común pasar los datos necesarios para la actualización dentro de una propiedadpayload
en el objeto de la acción. Ej:{ type: 'ADD', payload: { newItem } }
. - Exportar constantes de acción: Definir los
type
de las acciones como constantes (como en el ejemploACTIONS
) ayuda a evitar errores de tipeo y facilita el mantenimiento. - 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 elswitch
.- 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íaundefined
y tu aplicación se rompería.
- Solución: Si tu reducer recibe una acción que no reconoce, debería devolver el estado actual sin modificarlo. El
🚀 Retos prácticos
- Formulario Complejo: Convierte un formulario con varios campos (
useState
con un objeto) a unuseReducer
. Crea acciones paraUPDATE_FIELD
yRESET_FORM
. - Carrito de Compras: Gestiona un carrito de compras con acciones como
ADD_ITEM
,REMOVE_ITEM
,INCREMENT_QUANTITY
,DECREMENT_QUANTITY
yCLEAR_CART
. - 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.
- 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).
- 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);
-
useRef
devuelve un objeto con una única propiedad:current
. -
miRef.current
se inicializa convalorInicial
. - 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>
);
}
En este ejemplo:
-
inputRef
nos da acceso directo al<input>
, permitiéndonos llamar a.focus()
. -
renderCount
lleva la cuenta de las renderizaciones. Si usáramosuseState
para esto, cada actualización del contador causaría otro render, creando un bucle infinito. ConuseRef
, podemos actualizar el valor sin efectos secundarios.
✅ Buenas practicas y patrones comunes
- Cuándo usar
useRef
vsuseState
:- 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.
- Usa
-
Guardar el valor previo de props o estado: Un patrón muy útil es combinar
useRef
yuseEffect
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; // ... }
Manejo de Timers:
useRef
es ideal para guardar el ID desetInterval
osetTimeout
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
- Solución: Accede a la ref dentro de un
-
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.
- Solución: Si necesitas que un cambio se refleje en el render, ese valor debe estar en el estado (
🚀 Retos prácticos
- Reproductor de Video: Crea un componente que muestre un video (
<video>
). UsauseRef
para obtener una referencia al elemento y añade botones para "Play", "Pause" y "Stop". - Cronómetro Preciso: Haz un cronómetro que use
setInterval
. Guarda el ID del intervalo en una ref para poder detenerlo y reiniciarlo correctamente. - 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
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]);
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]);
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>
);
});
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>
);
}
✅ Buenas practicas y patrones comunes
- NO optimices prematuramente:
useMemo
yuseCallback
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). -
React.memo
es tu mejor amigo:useCallback
por sí solo no hace nada si el componente que recibe la función no está envuelto enReact.memo
(oPureComponent
). - 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.
- Solución: Para memorizar un componente, envuélvelo en
- 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
[]
.
- 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 enuseCallback
si se usa en varios sitios.
// BIEN ✅ const fetchData = useCallback(() => { /* ... */ }, []); useEffect(() => { fetchData(); }, [fetchData]);
- Solución: Define la función dentro del
🚀 Retos prácticos
- 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. - Lista de Hijos con
React.memo
: Crea un componente padre que renderiza una lista de 100 componentes hijos. Pasa una funciónonPress
al hijo. Comprueba con unconsole.log
cuántos hijos se re-renderizan cuando el padre cambia su estado. Luego, optimiza conuseCallback
yReact.memo
y comprueba la diferencia. - 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:
- Su nombre debe empezar con
use
(ej.useFetch
,useLocalStorage
). - 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 };
}
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>
);
}
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
- 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. - 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 (comouseState
), puedes devolver un array[valor, actualizador]
. - 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 elfetch
. - 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.
- Solución: Nombra siempre tus hooks personalizados como
- 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.
- 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
🚀 Retos prácticos
-
useLocalStorage
: Crea un hook que se sincronice conlocalStorage
. Debe funcionar de manera similar auseState
, pero persistiendo el valor en el navegador.
// Uso esperado: const [name, setName] = useLocalStorage('username', 'Invitado');
useOnlineStatus
: Crea un hook que devuelvatrue
si el navegador está conectado a internet yfalse
si no. (Pista:window.addEventListener('online', ...)
ywindow.addEventListener('offline', ...)
).-
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);
}
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]);
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>
);
}
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);
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, usauseMemo
. Para dependencias desetState
de unuseState
, puedes omitirlas, ya que React garantiza que son estables.
- Solución: Para funciones, usa
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:
- Cuando pasas props a un componente hijo envuelto en
React.memo
. - Para cálculos realmente pesados que están ralentizando tu componente.
- Para estabilizar una dependencia de otro hook (como
useEffect
).
- Cuando pasas props a un componente hijo envuelto en
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.
-
Documentación Oficial de React (en español)
- Introducción a los Hooks: La fuente de la verdad. Siempre actualizada y con excelentes explicaciones.
- Reglas de los Hooks: Lectura obligatoria.
-
Artículos y Guías Visuales
- A Complete Guide to useEffect (por Dan Abramov): Aunque en inglés, es la mejor explicación que existe sobre
useEffect
y su modelo mental. - Visualizing React Rendering: Ayuda a entender por qué y cuándo se re-renderizan los componentes.
- A Complete Guide to useEffect (por Dan Abramov): Aunque en inglés, es la mejor explicación que existe sobre
-
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)