Saltar al contenido
schedule 15 min React

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.

src/components/Personajes.jsx
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 useEffect directamente async. Por eso definimos la función async dentro y la llamamos inmediatamente. Esto es porque useEffect espera que la función devuelva undefined o 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:

src/components/PersonajesFiltro.jsx
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:

src/components/BuscadorAPI.jsx
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:

src/components/PersonajesPaginados.jsx
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:

src/hooks/useApi.js
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:

src/components/PersonajesSimple.jsx
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 fetch dentro de useEffect con [] para cargar datos al montar el componente.
  • El patrón loading/error/data con tres useState maneja los estados posibles de una petición.
  • Para filtrar en el cliente, usa estado derivado (filter sobre los datos cargados).
  • Para buscar en el servidor, usa useEffect con la búsqueda como dependencia y debounce con cleanup.
  • La paginación se implementa con un estado pagina como dependencia del useEffect.
  • 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.

code

Explorador galáctico con API

Avanzado schedule 30 min

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 useCharacters que 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.

Newsletter

Recibe nuevos cursos, actualizaciones, artículos del blog y promociones en tu correo.