Consumir una API
Hasta ahora los datos de tu aplicación vivían dentro del código. En el mundo real, los datos vienen de un servidor a través de APIs. En la sección de JavaScript ya aprendiste a usar fetch y async/await — ahora vas a combinarlo con React para cargar datos dinámicamente.
fetch dentro de useEffect
El patrón básico para cargar datos en React es: llamar a fetch dentro de un useEffect con array de dependencias vacío, para que se ejecute solo cuando el componente se monta.
import { useState, useEffect } from 'react'
function Personajes() {
const [personajes, setPersonajes] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
async function cargarPersonajes() {
try {
const respuesta = await fetch(
'https://rickandmortyapi.com/api/character'
)
if (!respuesta.ok) {
throw new Error(`Error HTTP: ${respuesta.status}`)
}
const datos = await respuesta.json()
setPersonajes(datos.results)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
cargarPersonajes()
}, []) // Array vacío = solo al montar
if (loading) return <p>Cargando personajes...</p>
if (error) return <p style={{ color: 'red' }}>Error: {error}</p>
return (
<div>
<h2>Personajes ({personajes.length})</h2>
<ul>
{personajes.map(p => (
<li key={p.id}>{p.name} — {p.status}</li>
))}
</ul>
</div>
)
}
export default Personajes
No puedes hacer el callback de
useEffectdirectamenteasync. Por eso definimos la funciónasyncdentro y la llamamos inmediatamente. Esto es porqueuseEffectespera que la función devuelvaundefinedo una función de cleanup, no una Promise.
El patrón loading / error / data
Siempre que consumas una API, maneja tres estados con tres variables:
const [data, setData] = useState(null) // Los datos
const [loading, setLoading] = useState(true) // ¿Estamos cargando?
const [error, setError] = useState(null) // ¿Hubo un error?
Y en el JSX, comprueba en este orden: loading → error → datos. Esto garantiza que nunca muestres datos incompletos ni un mensaje de error mientras cargas.
Filtrar datos del lado del cliente
Una vez que tienes los datos de la API, puedes filtrarlos en el cliente sin hacer otra petición:
import { useState, useEffect } from 'react'
function PersonajesFiltro() {
const [personajes, setPersonajes] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [busqueda, setBusqueda] = useState('')
useEffect(() => {
async function cargar() {
try {
const res = await fetch('https://rickandmortyapi.com/api/character')
if (!res.ok) throw new Error('Error al cargar')
const data = await res.json()
setPersonajes(data.results)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
cargar()
}, [])
// Filtro en el cliente: estado derivado
const personajesFiltrados = personajes.filter(p =>
p.name.toLowerCase().includes(busqueda.toLowerCase())
)
if (loading) return <p>Cargando...</p>
if (error) return <p style={{ color: 'red' }}>{error}</p>
return (
<div>
<input
value={busqueda}
onChange={(e) => setBusqueda(e.target.value)}
placeholder="Buscar personaje..."
/>
<p>{personajesFiltrados.length} resultado(s)</p>
<div className="grid">
{personajesFiltrados.map(p => (
<div key={p.id} className="card">
<img src={p.image} alt={p.name} width={100} />
<h3>{p.name}</h3>
<p>{p.status} — {p.species}</p>
</div>
))}
</div>
</div>
)
}
export default PersonajesFiltro
Buscar en el servidor con debounce
Para listas grandes, es mejor buscar directamente en la API. Combina useEffect con la búsqueda y un debounce para no hacer una petición por cada letra:
import { useState, useEffect } from 'react'
function BuscadorAPI() {
const [busqueda, setBusqueda] = useState('')
const [personajes, setPersonajes] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
// No buscar si está vacío
if (!busqueda.trim()) {
setPersonajes([])
return
}
setLoading(true)
setError(null)
// Debounce: esperar 500ms antes de buscar
const timeout = setTimeout(async () => {
try {
const res = await fetch(
`https://rickandmortyapi.com/api/character/?name=${busqueda}`
)
if (res.status === 404) {
setPersonajes([])
return
}
if (!res.ok) throw new Error('Error en la búsqueda')
const data = await res.json()
setPersonajes(data.results)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}, 500)
// Cleanup: cancela la búsqueda anterior si el usuario sigue escribiendo
return () => clearTimeout(timeout)
}, [busqueda])
return (
<div>
<input
value={busqueda}
onChange={(e) => setBusqueda(e.target.value)}
placeholder="Buscar en la API..."
/>
{loading && <p>Buscando...</p>}
{error && <p style={{ color: 'red' }}>{error}</p>}
{!loading && personajes.length === 0 && busqueda && (
<p>No se encontraron personajes.</p>
)}
<ul>
{personajes.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</div>
)
}
export default BuscadorAPI
Paginación
La API de Rick and Morty devuelve resultados paginados. El campo info tiene pages, next y prev:
import { useState, useEffect } from 'react'
function PersonajesPaginados() {
const [personajes, setPersonajes] = useState([])
const [info, setInfo] = useState(null)
const [pagina, setPagina] = useState(1)
const [loading, setLoading] = useState(true)
useEffect(() => {
async function cargar() {
setLoading(true)
try {
const res = await fetch(
`https://rickandmortyapi.com/api/character?page=${pagina}`
)
const data = await res.json()
setPersonajes(data.results)
setInfo(data.info)
} catch (err) {
console.error(err)
} finally {
setLoading(false)
}
}
cargar()
}, [pagina]) // Se re-ejecuta cuando cambia la página
return (
<div>
<h2>Personajes</h2>
{loading ? (
<p>Cargando...</p>
) : (
<>
<div className="grid">
{personajes.map(p => (
<div key={p.id}>
<img src={p.image} alt={p.name} width={80} />
<p>{p.name}</p>
</div>
))}
</div>
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'center' }}>
<button
onClick={() => setPagina(prev => prev - 1)}
disabled={pagina <= 1}
>
← Anterior
</button>
<span>Página {pagina} de {info?.pages}</span>
<button
onClick={() => setPagina(prev => prev + 1)}
disabled={!info?.next}
>
Siguiente →
</button>
</div>
</>
)}
</div>
)
}
export default PersonajesPaginados
Fíjate: [pagina] como dependencia del useEffect hace que se ejecute una nueva petición cada vez que el usuario cambia de página.
Custom hook: useApi
Si repites el patrón loading/error/data en varios componentes, puedes extraerlo a un custom hook:
import { useState, useEffect } from 'react'
function useApi(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
if (!url) return
async function fetchData() {
setLoading(true)
setError(null)
try {
const res = await fetch(url)
if (!res.ok) throw new Error(`Error: ${res.status}`)
const json = await res.json()
setData(json)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
fetchData()
}, [url])
return { data, loading, error }
}
export default useApi
Ahora cualquier componente puede consumir una API en una sola línea:
import useApi from '../hooks/useApi'
function PersonajesSimple() {
const { data, loading, error } = useApi(
'https://rickandmortyapi.com/api/character'
)
if (loading) return <p>Cargando...</p>
if (error) return <p>Error: {error}</p>
return (
<ul>
{data.results.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
)
}
export default PersonajesSimple
Un custom hook es simplemente una función que empieza con use y puede usar otros hooks dentro. Es la forma de React de reutilizar lógica entre componentes — igual que las funciones reutilizables que aprendiste en Lógica de Programación, pero con superpoderes reactivos.
Resumen
- Usa
fetchdentro deuseEffectcon[]para cargar datos al montar el componente. - El patrón loading/error/data con tres
useStatemaneja los estados posibles de una petición. - Para filtrar en el cliente, usa estado derivado (
filtersobre los datos cargados). - Para buscar en el servidor, usa
useEffectcon la búsqueda como dependencia y debounce con cleanup. - La paginación se implementa con un estado
paginacomo dependencia deluseEffect. - Los custom hooks (
useApi) encapsulan lógica reutilizable que usa otros hooks.
En la siguiente lección aprenderás React Router — navegación entre diferentes páginas sin recargar el navegador, rutas dinámicas y parámetros de URL.
Explorador galáctico con API
Construye un explorador de personajes de Rick and Morty que combine todo lo aprendido:
- Al montar el componente, carga los personajes de
https://rickandmortyapi.com/api/character. - Muestra cada personaje en una tarjeta con su imagen, nombre, estado (Alive/Dead/Unknown con indicador de color) y especie.
- Implementa búsqueda en el servidor con debounce de 500ms usando el parámetro
?name=de la API. - Implementa paginación con botones "Anterior" y "Siguiente", mostrando "Página X de Y".
- Crea un custom hook
useCharactersque encapsule toda la lógica de fetching, búsqueda y paginación. - Maneja los estados de loading, error y lista vacía con mensajes apropiados.
Bonus: añade filtros por estado (Alive, Dead, Unknown) usando el parámetro ?status= de la API.
lightbulb Pistas
Tu custom hook useCharacters debería recibir page y name como parámetros (o construir la URL internamente). Para el debounce, usa un useEffect separado que actualice un estado debouncedSearch después de 500ms, y otro useEffect que haga el fetch cuando cambie debouncedSearch o page. Para los indicadores de color: verde para Alive, rojo para Dead, gris para Unknown.