Lección 13 de 13 · Lectura y práctica
Patrones comunes de useEffect
Patrones practicos Fetch con async/await function Users() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { // No puedes hacer.
Del concepto a la práctica
Taller de esta lección
Una búsqueda local con retraso combina estado de entrada, efecto y limpieza del temporizador. Si el usuario escribe varias letras rápidamente, solo conviene procesar el valor que permanezca estable durante el intervalo. El resultado se calcula desde la colección y la consulta estable; solo el temporizador exige limpieza.
Ejemplo resuelto
El bloque autónomo revisado se encuentra más abajo.
Con tres libros y consulta mar, si dos títulos contienen “mar”, la salida es 2 resultados.
Pruébalo
Define tres títulos conocidos.
Cambia la consulta con un campo.
Espera 300 ms tras escribir.
Comprueba el número filtrado.
Comprueba tu resultado
La consulta vacía debe devolver los tres libros.
Error frecuente: omitir la limpieza y acumular temporizadores; corrígelo con clearTimeout(timer). No guardes un nombre completo derivado si puedes construirlo durante el render.
Debounce sin resultado duplicado
El componente completo siguiente difiere el valor de consulta y calcula la colección filtrada durante render; no guarda una segunda copia de resultados. El temporizador es el recurso externo que se limpia. El input del fragmento anterior estaba ausente y ahora permite probar la interacción.
import {useEffect,useState} from 'react';
const LIBROS=[{id:'b1',titulo:'Mar abierto'},{id:'b2',titulo:'Mareas'},{id:'b3',titulo:'Ríos'}];
export default function Buscar(){
const [consulta,setConsulta]=useState('');
const [estable,setEstable]=useState('');
useEffect(()=>{
const timer=setTimeout(()=>setEstable(consulta),300);
return ()=>clearTimeout(timer);
},[consulta]);
const q=estable.trim().toLowerCase();
const resultados=LIBROS.filter(l=>l.titulo.toLowerCase().includes(q));
return <section>
<label>Buscar<input value={consulta} onChange={e=>setConsulta(e.target.value)}/></label>
<p>Filtro aplicado: {estable || 'Todos'}</p>
<p>{resultados.length} resultados</p>
</section>;
}Vacía da3, mar da2, ríos da1 y zzz da0 después del intervalo. Durante espera se muestra el filtro anterior con su etiqueta;300ms es el mínimo solicitado al temporizador, no un SLA exacto. Una API real añade validación de HTTP/esquema y cancelación o descarte de respuestas obsoletas; limpiar solo el timeout no cancela una petición ya iniciada. localStorage puede fallar o contener JSON inválido: valida y captura errores antes de hidratar, y distingue almacenamiento de sesión de una confirmación de servidor.

Mapa de prácticaAl cambiar la consulta, B pasa a ser la petición vigente. Si B termina antes y A llega después, se muestra B y se descarta A; una respuesta antigua tampoco debe cambiar el estado de carga actual.
Ver la infografía en tamaño completo ↗Patrones practicos
Fetch con async/await
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// No puedes hacer useEffect async directamente
// Crea una funcion async dentro
const fetchUsers = async () => {
try {
setLoading(true);
const res = await fetch('/api/users');
if (!res.ok) throw new Error('Error al cargar');
const data = await res.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) return <p>Cargando...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
Debounce de busqueda
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
// Espera 300ms antes de buscar
const timer = setTimeout(() => {
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(setResults);
}, 300);
return () => clearTimeout(timer);
}, [query]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Buscar..."
/>
<ul>
{results.map(r => <li key={r.id}>{r.title}</li>)}
</ul>
</div>
);
}
Local Storage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Uso
function App() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Tema: {theme}
</button>
);
}
Evitar efectos innecesarios
// MAL - efecto innecesario
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
}
// BIEN - calcula durante el render
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// Derivado, no necesita estado
const fullName = firstName + ' ' + lastName;
}Sigue con React Fundamentals
Esta lección forma parte de un curso de Cursalo. Revisa su programa y elige entre la compra individual o el acceso a toda la escuela.
Si ya tenías acceso a este curso, entra con tu cuenta: tus derechos y tu progreso se conservan. No necesitas comprarlo de nuevo.