learning-front

Nivel 6 · React de cero a héroe (con TypeScript)

Salir del playground: tu primer fetch real

Todo el nivel corrió con un cargarHeroes() simulado. Aquí haces un fetch real —en este mismo playground— contra una API pública, dentro de un componente React: con su estado de carga, su error, la validación de la respuesta con Zod y el muro de CORS.

Has construido un Team Builder entero en React, con routing, estado global, formularios y patrones. Pero una mentira piadosa ha sostenido todo el nivel: los datos nunca salieron de tu ordenador. El cargarHeroes() que usabas era un array fijo detrás de un setTimeout —tardaba “de mentira” y no fallaba nunca—. Este capítulo, corto, cambia esa promesa simulada por un fetch de verdad. Y lo bueno: corre en este mismo playground, porque vamos a pedir a una API pública con CORS abierto.

El fetch real, ahora dentro de React#

En “JSON y fetch” (Nivel 3) ya hiciste un fetch real contra una API pública, pero en la consola y con datos sueltos. Aquí lo llevas a donde vive en una app: dentro de un componente, con useEffect para lanzarlo y useState para guardar lo que llega y el estado de carga.

tsx
// La URL de la API. En el playground va aquí; en un proyecto real iría en .env (Nivel 4).
const API_URL = "https://jsonplaceholder.typicode.com/users";

function useRoster() {
  const [heroes, setHeroes] = useState<Heroe[]>([]);
  const [cargando, setCargando] = useState(true);

  useEffect(() => {
    // fetch sale a la red de verdad; resuelve con un Response, no con los datos.
    fetch(API_URL)
      // res.json() lee y parsea el cuerpo (otra promesa).
      .then((res) => res.json())
      .then((datos) => {
        // La API devuelve SU forma (name); la adaptamos a la nuestra (nombre).
        setHeroes(datos.map((u) => ({ id: u.id, nombre: u.name })));
        setCargando(false);
      });
  }, []);

  return { heroes, cargando };
}

Ese último paso es nuevo e importante: una API pública no te da héroes de Overwatch con tu forma exacta. Te da lo que tiene, y tú lo adaptas a tu dominio. Y como lo que llega de fuera no es de fiar, en cuanto puedas lo validas con Zod (lo viste en el Nivel 5): si la forma no encaja, parse() falla ahí mismo, con un mensaje claro, en vez de dejar que un dato corrupto se cuele y reviente lejos.

Pruébalo#

Esto que ves cargando sale de una API pública de verdad, ahora mismo, desde el playground. Recarga la demo y verás el “Cargando…” un instante antes de que lleguen los datos —eso es la red, que ya no es instantánea—.

La red falla, y la UI tiene que contarlo#

Aquí está el “¿y qué?” que el playground escondía. Una red real falla: estás sin conexión, el servidor devuelve un 500, la petición caduca. Y hay una trampa que ya conoces del Nivel 3: cuando la API responde con un 404 o un 500, fetch no rechaza la promesa —eso es una respuesta HTTP válida—, así que tu código sigue tan tranquilo con un Response de error en la mano. Hay que comprobarlo:

tsx
const res = await fetch(API_URL);
// res.ok es true solo si el estado es 2xx. Sin esta línea, un 500 se cuela como un éxito.
if (!res.ok) throw new Error("La API respondió " + res.status);

En el playground, ese estado de error era decorativo: la promesa simulada no fallaba nunca. Con una red real, es la mitad de tu UI. Un try/catch que recoge el fallo y lo enseña, y un botón de “reintentar”, dejan de ser un lujo: sin ellos, un fallo deja al usuario mirando un “Cargando…” eterno, sin saber qué pasó ni cómo salir.

CORS: el muro del navegador#

Y hay un fallo nuevo que el playground no te enseñó, porque la demo de arriba lo esquiva por suerte. Cambia la URL por la de otra API cualquiera y es muy probable que, en lugar de datos, te salte un error rojo de CORS en la consola.

CORS es una protección del navegador, no de la API. La demo de arriba funciona porque jsonplaceholder responde con una cabecera (Access-Control-Allow-Origin: *) que permite a cualquier origen leer su respuesta. Una API que no la mande, no. Y lo incómodo: tu petición llega al servidor —por eso un curl desde la terminal (que no aplica CORS) funcionaría—, pero el navegador te esconde la respuesta. El “¿y qué?”: no se arregla desde el frontend. No hay header mágico que añadir a tu fetch. O la API permite tu origen, o tienes que enrutar la petición por un backend propio (que llama a la API sin un navegador de por medio). Es la frontera real entre lo que el frontend puede hacer solo y lo que necesita un servidor detrás.

Comprueba lo que sabes#

Pregunta 1 de 3

Tu fetch a la API recibe un 500, pero tu código sigue adelante y revienta al usar los datos. ¿Por qué?

Tu turno#

El starter trae el roster simulado de todo el nivel. Cámbialo por un fetch real contra la API pública (la URL ya está puesta) y sube de tier: primero que los datos sean reales, luego controla res.ok y valida con Zod, y al final añade reintento. Compara cada tier con su solución.

Ejercicio · en esta página

Tu primer fetch real

El starter trae el roster SIMULADO que has usado todo el nivel. Cámbialo por un fetch real contra la API pública (la URL ya está en el fichero). Adapta la forma de la API a la tuya, gestiona la carga y el error, y sube de tier según la robustez.

Paso 1: Datos reales en pantalla

  • El roster ya no es un array fijo: viene de un fetch real con useEffect + useState
  • La forma de la API (name) se adapta a la tuya (nombre)
  • Mientras la red trabaja se muestra un estado de carga
Ver soluciones
import { useEffect, useState } from "react";

// API pública de pruebas (la misma de "JSON y fetch", Nivel 3); CORS abierto.
// En un proyecto real, esta URL iría en una variable de entorno (.env, Nivel 4).
const API_URL = "https://jsonplaceholder.typicode.com/users";

interface Heroe {
  id: number;
  nombre: string;
}

export default function App() {
  const [heroes, setHeroes] = useState<Heroe[]>([]);
  const [cargando, setCargando] = useState(true);

  useEffect(() => {
    // fetch REAL: sale a la red de verdad. Devuelve una promesa que resuelve con un Response.
    fetch(API_URL)
      // res.json() lee y parsea el cuerpo de la respuesta (otra promesa).
      .then((res) => res.json())
      .then((datos) => {
        // La API devuelve SU forma (name, email...); la adaptamos a la nuestra (id, nombre).
        const adaptados = datos.map((u: { id: number; name: string }) => ({
          id: u.id,
          nombre: u.name,
        }));
        setHeroes(adaptados);
        // Ya hay datos: quitamos el estado de carga.
        setCargando(false);
      });
  }, []);

  // Mientras la red trabaja, mostramos un aviso de carga (ya no es instantáneo como el array fijo).
  if (cargando) return <p className="estado">Cargando héroes de la API…</p>;

  return (
    <section>
      <h1 className="titulo">Roster ({heroes.length})</h1>
      <ul className="lista">
        {heroes.map((h) => (
          <li key={h.id} className="tarjeta">{h.nombre}</li>
        ))}
      </ul>
    </section>
  );
}

Por qué este nivel

  • El cambio clave: el roster ya no es un array fijo, sale de un fetch real dentro de un useEffect. Ya hiciste un fetch en JSON y fetch (Nivel 3), pero en la consola; aquí vive donde le toca —en un componente—, y su resultado pinta la UI.
  • Dos cosas que el playground escondía aparecen de inmediato: la respuesta tarda (de ahí el estado de carga) y llega con la forma de la API (name), que adaptas a la tuya (nombre).