Llamada a la API: Consumiendo datos reales con fetch
Hasta ahora hemos trabajado con datos estáticos almacenados en archivos locales. En esta clase aprenderás a consumir una API real, gestionar el estado de carga, manejar errores y mejorar la experiencia del usuario mientras se obtienen los datos del servidor.
El problema: Datos estáticos vs datos dinámicos
Trabajando con datos estáticos
Hasta ahora, nuestros datos venían de un archivo:
// jobs.json
const jobs = [
{ id: 1, title: 'Frontend Developer', company: 'Acme Corp', location: 'Remote' },
{ id: 2, title: 'Backend Engineer', company: 'TechCo', location: 'Madrid' },
// ...
]
// App.jsx
import jobsData from './data/jobs.json'
function App() {
const [jobs] = useState(jobsData)
return (
<div>
{jobs.map((job) => (
<JobCard key={job.id} job={job} />
))}
</div>
)
}
❌ Problemas con datos estáticos:
- Los datos nunca cambian (no reflejan la realidad)
- No se pueden filtrar en el backend
- No hay paginación real
- No escala para grandes volúmenes de datos
- No representa cómo funcionan las aplicaciones reales
Datos dinámicos desde una API
En aplicaciones reales, los datos vienen de un servidor:
function App() {
const [jobs, setJobs] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/jobs')
.then((response) => response.json())
.then((data) => {
setJobs(data)
setLoading(false)
})
}, [])
if (loading) return <div>Cargando empleos...</div>
return (
<div>
{jobs.map((job) => (
<JobCard key={job.id} job={job} />
))}
</div>
)
}
✅ Ventajas:
- ✨ Datos actualizados en tiempo real
- 🔍 Filtros procesados en el servidor
- 📄 Paginación eficiente
- 🚀 Escalable para cualquier cantidad de datos
- 💼 Refleja aplicaciones profesionales
Revisión: ¿Cuál era el custom hook correcto?
Antes de comenzar con la API, hagamos una reflexión sobre el custom hook que creamos anteriormente.
El hook que NO debimos crear
// ❌ Este hook NO es el correcto
function useSomeLogic() {
// Lógica demasiado simple o no relacionada con filtros
// Solo algunas funciones helper
}
El hook correcto: useFilters
El custom hook que realmente debimos crear es useFilters, porque contiene la lógica pesada del componente:
// ✅ Este es el hook correcto
function useFilters() {
const [filters, setFilters] = useState({
text: '',
type: '',
location: '',
})
const [page, setPage] = useState(1)
const [limit] = useState(10)
const updateFilter = (name, value) => {
setFilters((prev) => ({ ...prev, [name]: value }))
}
const nextPage = () => setPage((prev) => prev + 1)
const prevPage = () => setPage((prev) => Math.max(1, prev - 1))
return {
filters,
updateFilter,
page,
nextPage,
prevPage,
limit,
}
}
¿Por qué este hook es el correcto?
- ✅ Encapsula toda la lógica de estados relacionados
- ✅ Gestiona múltiples estados (filtros, paginación)
- ✅ Proporciona funciones para modificar el estado
- ✅ Es reutilizable en otros componentes
¿Por qué no incluir el useEffect del título en el hook?
Podrías pensar: “¿Por qué no meter el useEffect que cambia el título dentro del hook?”
// ❌ No hacer esto
function useFilters() {
const [filters, setFilters] = useState({})
// Esto NO pertenece aquí
useEffect(() => {
document.title = `Empleos - Filtros aplicados`
}, [filters])
return { filters, setFilters }
}
Respuesta: Ese efecto no tiene nada que ver con aplicar los filtros.
- El efecto cambia el título de la página
- Los filtros gestionan los criterios de búsqueda
- Son responsabilidades diferentes
Regla de oro para custom hooks
Un hook debe representar algo concreto y cohesivo. No mezcles lógicas no relacionadas.
✅ Hacer:
function useFilters() {
// Todo relacionado con filtros
}
function useDocumentTitle(title) {
// Todo relacionado con el título del documento
useEffect(() => {
document.title = title
}, [title])
}
Presentación de la API
En este módulo usaremos una API real de empleos que ya está creada y desplegada:
GET https://jscamp-api.vercel.app/api/jobs
Características de la API
La API soporta los siguientes parámetros de búsqueda:
| Parámetro | Tipo | Descripción | Ejemplo |
|---|---|---|---|
limit | number | Cantidad de resultados por página | 10 |
offset | number | Desde qué resultado empezar | 0, 10, 20 |
text | string | Búsqueda por texto libre | "react" |
technology | string | Filtrar por tecnología | "react", "node" |
type | string | Filtrar por modalidad/ubicación | "remoto", "madrid" |
level | string | Filtrar por nivel de experiencia | "junior", "senior" |
Ejemplo de respuesta real
{
"total": 34,
"limit": 10,
"offset": 0,
"results": 10,
"data": [
{
"id": "7a4d1d8b-1e45-4d8c-9f1a-8c2f9a9121a4",
"titulo": "Desarrollador de Software Senior",
"empresa": "Tech Solutions Inc.",
"ubicacion": "Remoto",
"descripcion": "Buscamos un ingeniero de software con experiencia en desarrollo web y conocimientos en JavaScript, React y Node.js. El candidato ideal debe ser capaz de trabajar en equipo y tener buenas habilidades de comunicación.",
"data": {
"technology": ["react", "node", "javascript"],
"modalidad": "remoto",
"nivel": "senior"
}
},
{
"id": "d35b2c89-5d60-4f26-b19a-6cfb2f1a0f57",
"titulo": "Analista de Datos",
"empresa": "Data Driven Co.",
"ubicacion": "Ciudad de México",
"descripcion": "Estamos buscando un analista de datos con experiencia en el manejo de grandes conjuntos de datos y herramientas de visualización. Se requiere conocimiento en SQL, Python y R.",
"data": {
"technology": ["python", "sql", "r", "pandas"],
"modalidad": "cdmx",
"nivel": "junior"
}
}
// ... más empleos
]
}
Estructura de la respuesta:
total: Total de empleos disponibles (34 en este caso)limit: Cuántos empleos se devuelven por página (10)offset: Desde qué posición se empieza (0)results: Cantidad de resultados en esta respuesta (10)data: Array con los empleos
Estructura de cada empleo:
id: Identificador único (UUID)titulo: Título del puestoempresa: Nombre de la empresaubicacion: Ubicación del trabajodescripcion: Descripción detallada del puestodata: Objeto con metadatatechnology: Array de tecnologías requeridasmodalidad: Modalidad de trabajo (remoto, presencial, híbrido)nivel: Nivel de experiencia requerido
💡 Puedes probar la API directamente en tu navegador visitando https://jscamp-api.vercel.app/api/jobs
Preparando el estado para la API
Antes de hacer la llamada, necesitamos preparar varios estados:
function JobSearch() {
// Estado para los empleos (inicialmente vacío)
const [jobs, setJobs] = useState([])
// Estado para indicar que estamos cargando
const [loading, setLoading] = useState(true)
// Estado para el total de resultados
const [total, setTotal] = useState(0)
return (
<div>{loading ? <div>Cargando empleos...</div> : <JobListing jobs={jobs} total={total} />}</div>
)
}
¿Por qué tres estados?
1. jobs → Array con los empleos
- Inicialmente vacío
[] - Se llena cuando la API responde
2. loading → Booleano para estado de carga
truemientras esperamos la respuestafalsecuando ya tenemos los datos- Mejora la UX mostrando un indicador de carga
3. total → Número total de resultados
- Necesario para la paginación
- Permite calcular cuántas páginas hay
¿Por qué NO podemos hacer fetch directamente en el render?
Podrías pensar: “¿Por qué no hacer fetch() directamente en el cuerpo del componente?”
// ❌ NUNCA hagas esto
function JobSearch() {
const [jobs, setJobs] = useState([])
// ¡ERROR! Esto causará un loop infinito
fetch('/api/jobs')
.then((response) => response.json())
.then((data) => setJobs(data)) // Esto causa un re-render
// El re-render vuelve a ejecutar fetch()
// Que vuelve a causar un re-render
// ¡Loop infinito! 💥
return <JobListing jobs={jobs} />
}
El problema del render
React ejecuta el cuerpo del componente en cada render:
1. Componente se renderiza
2. fetch() se ejecuta
3. Respuesta llega → setJobs()
4. setJobs() causa un re-render
5. Volvemos al paso 1 → ¡Loop infinito!
La solución: useEffect
Para efectos secundarios (como llamadas a APIs) siempre usa useEffect:
function JobSearch() {
const [jobs, setJobs] = useState([])
useEffect(() => {
// Esto solo se ejecuta cuando lo indiquen las dependencias
fetch('https://jscamp-api.vercel.app/api/jobs')
.then((response) => response.json())
.then((data) => setJobs(data))
}, []) // Array vacío = solo al montar el componente
return <JobListing jobs={jobs} />
}
Implementando el fetch en un efecto
Vamos a implementar correctamente la llamada a la API:
function JobSearch() {
const [jobs, setJobs] = useState([])
const [loading, setLoading] = useState(true)
const [total, setTotal] = useState(0)
useEffect(() => {
// Función asíncrona dentro del efecto
async function fetchJobs() {
try {
// 1. Indicar que estamos cargando
setLoading(true)
// 2. Hacer la petición
const response = await fetch('https://jscamp-api.vercel.app/api/jobs')
const json = await response.json()
// 3. Guardar los datos
setJobs(json.data)
setTotal(json.total)
} catch (error) {
// 4. Manejar errores
console.error('Error al cargar empleos:', error)
} finally {
// 5. Indicar que terminamos de cargar
setLoading(false)
}
}
// Llamar a la función
fetchJobs()
}, []) // Dependencias vacías = solo al montar
if (loading) return <div>Cargando empleos...</div>
return <JobListing jobs={jobs} total={total} />
}
¿Por qué no usamos directamente el callback de useEffect como
async? Porque el callback de useEffect siempre tiene que ser una función sincrónica, por lo que no podemos usarasyncdirectamente. Por eso creamos una función asíncrona dentro del efecto.
Desglosando el código
1. Función fetchJobs dentro del efecto
async function fetchJobs() {
// Lógica de fetch aquí
}
¿Por qué definir la función dentro del efecto?
- ✅ Mantiene la lógica cerca de donde se usa
- ✅ Evita que sea accesible desde otros lugares
- ✅ Tiene acceso directo a los estados del componente
- ✅ Es más fácil de leer y mantener
2. Estados de carga
setLoading(true) // Al inicio
// ... hacer fetch
setLoading(false) // Al terminar
Esto permite mostrar feedback al usuario mientras esperamos la respuesta.
3. Manejo de la respuesta
const response = await fetch('/api/jobs')
const json = await response.json()
setJobs(json.data) // Solo el array de empleos
setTotal(json.total) // El total de resultados
La API devuelve un objeto con { data, total, limit, offset }, pero solo necesitamos data y total.
4. Try-catch para errores
try {
// Intentar hacer fetch
} catch (error) {
console.error('Error:', error)
}
Siempre maneja posibles errores en llamadas asíncronas.
5. Finally para cleanup
finally {
setLoading(false)
}
Sin importar si hubo éxito o error, siempre indicamos que terminamos de cargar.
Corrección del error: jobs.map is not a function
Al implementar el fetch, es común encontrar este error:
TypeError: jobs.map is not a function
¿Por qué ocurre?
La API devuelve:
{
"total": 143,
"limit": 10,
"offset": 0,
"data": [
/* empleos aquí */
]
}
Si haces esto:
// ❌ Error: guardas todo el objeto
setJobs(json) // json es un objeto, no un array
// Luego en el render:
jobs.map(job => ...) // ¡Error! jobs es un objeto, no tiene .map()
Solución: Separar datos
// ✅ Correcto: guardar solo el array
const response = await fetch('/api/jobs')
const json = await response.json()
setJobs(json.data) // Solo el array con los resultados
setTotal(json.total) // El número total en otro estado
Ahora:
jobses un array → puede usar.map()totales un número → podemos usarlo para paginación
Verificando que los datos vienen de la API
Una vez implementado, puedes verificar que todo funciona:
1. Abrir DevTools → Network
- Haz clic en la pestaña Network (Red)
- Recarga la página
- Verás una petición a
/api/jobs
2. Inspeccionar la petición
Request URL: https://jscamp-api.vercel.app/api/jobs
Request Method: GET
Status Code: 200 OK
Response:
{
"total": 143,
"limit": 10,
"offset": 0,
"data": [...]
}
3. Verificar los datos en pantalla
Los empleos mostrados ahora vienen de la API, no del archivo estático:
- ✅ Si actualizas la base de datos, los cambios se reflejan
- ✅ Los filtros se procesarán en el backend (próxima clase)
- ✅ La paginación será real, no solo frontend
Simulando una conexión lenta
Para ver mejor el estado de carga, puedes simular una red lenta:
Opción 1: En DevTools
- Abre DevTools → Network
- Cambia “No throttling” a “Slow 3G”
- Recarga la página
Verás que tarda mucho más en cargar, y el estado loading es visible.
Opción 2: Delay artificial en código
Para desarrollo, puedes añadir un delay artificial:
useEffect(() => {
async function fetchJobs() {
try {
setLoading(true)
// Delay artificial de 5 segundos
await new Promise((resolve) => setTimeout(resolve, 5000))
const response = await fetch('/api/jobs')
const json = await response.json()
setJobs(json.data)
setTotal(json.total)
} catch (error) {
console.error(error)
} finally {
setLoading(false)
}
}
fetchJobs()
}, [])
Ahora verás claramente:
- Pantalla muestra “Cargando empleos…” durante 5 segundos
- Luego aparecen los datos
⚠️ Recuerda quitar el delay antes de subir a producción.
Mejorando la UX con un indicador de carga
Sin un indicador de carga, el usuario ve una pantalla en blanco y no sabe qué está pasando.
Antes (sin indicador)
function JobSearch() {
const [jobs, setJobs] = useState([])
useEffect(() => {
fetch('https://jscamp-api.vercel.app/api/jobs')
.then((r) => r.json())
.then((data) => setJobs(data.data))
}, [])
return <JobListing jobs={jobs} />
}
Problema: Mientras carga la API, jobs es un array vacío → la página muestra nada.
Después (con indicador)
function JobSearch() {
const [jobs, setJobs] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
async function fetchJobs() {
setLoading(true)
const response = await fetch('https://jscamp-api.vercel.app/api/jobs')
const data = await response.json()
setJobs(data.data)
setLoading(false)
}
fetchJobs()
}, [])
if (loading) {
return (
<div className="loading">
<p>Cargando empleos...</p>
<div className="spinner" />
</div>
)
}
return <JobListing jobs={jobs} />
}
Ventaja: El usuario sabe que algo está pasando, mejorando la percepción de la aplicación.
El ciclo completo de carga
Entendamos todo el flujo paso a paso:
1. Componente se monta
└─> loading = true
└─> jobs = []
2. useEffect se ejecuta
└─> Llama a fetchJobs()
3. fetchJobs() hace la petición
└─> Usuario ve: "Cargando empleos..."
└─> Esperando respuesta del servidor...
4. API responde
└─> setJobs(data.data)
└─> setTotal(data.total)
└─> setLoading(false)
5. React re-renderiza
└─> loading ahora es false
└─> Se renderiza JobListing con los empleos
Conceptos clave aprendidos
| Concepto | Descripción |
|---|---|
| Fetch en useEffect | Las llamadas asíncronas deben estar dentro de useEffect, nunca en el render directo |
| Estado de carga | Usar loading para mostrar feedback mientras esperamos la respuesta |
| Separación de datos | Distinguir entre el objeto completo de la API y los datos que necesitamos |
| Gestión de errores | Siempre usar try-catch en operaciones asíncronas |
| Dependencias vacías | [] en useEffect = ejecutar solo al montar el componente |
| UX durante carga | Nunca dejar al usuario con una pantalla en blanco |
Buenas prácticas
✅ Siempre usa try-catch en llamadas asíncronas
✅ Gestiona el estado de carga para feedback al usuario
✅ Maneja errores apropiadamente con estados y mensajes
✅ Usa loading/finally para garantizar que el estado se actualice
✅ Separa responsabilidades entre diferentes hooks cuando sea necesario
❌ NUNCA hagas fetch directamente en el render
❌ NO mezcles lógicas no relacionadas en un mismo hook
❌ NO ignores los errores de las peticiones
Próximos pasos
En las siguientes clases aprenderemos:
- Filtros dinámicos: Enviar parámetros a la API según los filtros del usuario
- Paginación real: Usar
limityoffsetpara cargar páginas - Debouncing: Evitar hacer peticiones en cada tecla
Conclusión
En esta clase has aprendido a:
- ✅ Consumir una API real usando
fetch - ✅ Gestionar estados de carga con
loading - ✅ Manejar errores en peticiones asíncronas
- ✅ Entender por qué usar
useEffectpara llamadas a APIs - ✅ Separar correctamente los datos de la respuesta
- ✅ Mejorar la UX mostrando indicadores de carga
- ✅ Evitar loops infinitos en llamadas asíncronas
- ✅ Identificar qué lógica debe ir en qué custom hook
Has dado un paso crucial: pasar de datos estáticos a datos dinámicos. Ahora tu aplicación React puede comunicarse con un backend real, lo cual es fundamental para aplicaciones profesionales.
💡 Recuerda: En las próximas clases integraremos filtros y paginación con la API, completando el ciclo de una aplicación real.