28. Obtención de datos desde una API

 

Tutorial: Obtención de Datos desde una API en React Native

🌐 Introducción a las APIs REST

En el desarrollo de aplicaciones móviles modernas, es esencial conectar con servidores en la nube para obtener, enviar y manipular datos. Hoy aprenderemos a consumir APIs REST usando dos métodos populares en React Native.

API Utilizada: Rick and Morty API

  • API pública y gratuita

  • Soporta REST y GraphQL

  • Contiene datos de personajes, locaciones y episodios


🛠️ Paso 1: Configuración del Proyecto

Estructura de carpetas:

text

src/

├── api/

│   ├── RickAndMortyApi.ts      # Método con Fetch

│   ├── RickAndMortyApiAlt.ts   # Método con Axios

│   └── entities/               # Interfaces TypeScript

│       ├── ApiInfoResponse.ts

│       └── CharacterResponse.ts

└── screens/

    └── Profile.tsx

Instalación de dependencias:

bash

# Para usar Axios necesitamos instalarlo

npm install axios

# o

yarn add axios


🔄 Paso 2: Método 1 - Usando Fetch (Nativo de React Native)

RickAndMortyApi.ts:

typescript

import { ApiInfoResponse } from './entities/ApiInfoResponse';


/**

 * Obtiene información general de la API usando Fetch

 * @returns Promise con la respuesta de la API

 */

export const getApiInfo = (): Promise<Response> => {

  return fetch('https://rickandmortyapi.com/api', {

    method: 'GET',

    // Podemos agregar headers si es necesario

    headers: {

      'Content-Type': 'application/json',

    },

  });

};


/**

 * Obtiene personajes usando Fetch (alternativa)

 */

export const getCharactersWithFetch = async (page: number = 1): Promise<any> => {

  try {

    const response = await fetch(

      `https://rickandmortyapi.com/api/character?page=${page}`,

      {

        method: 'GET',

        headers: {

          'Content-Type': 'application/json',

        },

      }

    );

    

    if (!response.ok) {

      throw new Error(`Error: ${response.status}`);

    }

    

    return await response.json();

  } catch (error) {

    console.error('Error fetching characters:', error);

    throw error;

  }

};


📦 Paso 3: Método 2 - Usando Axios (Librería Externa)

RickAndMortyApiAlt.ts:

typescript

import axios from 'axios';

import { CharacterResponse } from './entities/CharacterResponse';


// Crear instancia de Axios con configuración base

const apiClient = axios.create({

  baseURL: 'https://rickandmortyapi.com/api',

  timeout: 10000, // 10 segundos timeout

  headers: {

    'Content-Type': 'application/json',

  },

});


/**

 * Obtiene personajes usando Axios

 * @param page Número de página (la API está paginada)

 * @returns Promise con los personajes

 */

export const getCharacters = async (

  page: number = 1

): Promise<CharacterResponse> => {

  try {

    const response = await apiClient.get<CharacterResponse>(

      `/character?page=${page}`

    );

    return response.data;

  } catch (error) {

    console.error('Error fetching characters with Axios:', error);

    throw error;

  }

};


/**

 * Obtiene información de un personaje específico

 */

export const getCharacterById = async (

  id: number

): Promise<Character> => {

  const response = await apiClient.get<Character>(

    `/character/${id}`

  );

  return response.data;

};


📝 Paso 4: Definir Interfaces TypeScript

entities/ApiInfoResponse.ts:

typescript

/**

 * Interfaz para la respuesta de información de la API

 */

export interface ApiInfoResponse {

  characters: string;   // URL del endpoint de personajes

  locations: string;    // URL del endpoint de locaciones

  episodes: string;     // URL del endpoint de episodios

}

entities/CharacterResponse.ts:

typescript

/**

 * Interfaz para la información de paginación

 */

export interface Info {

  count: number;    // Total de personajes (826)

  pages: number;    // Total de páginas (42)

  next: string | null;     // URL de siguiente página

  prev: string | null;     // URL de página anterior

}


/**

 * Interfaz para un personaje individual

 */

export interface Character {

  id: number;

  name: string;

  status: 'Alive' | 'Dead' | 'unknown';

  species: string;

  type: string;

  gender: 'Female' | 'Male' | 'Genderless' | 'unknown';

  origin: {

    name: string;

    url: string;

  };

  location: {

    name: string;

    url: string;

  };

  image: string;    // URL de la imagen

  episode: string[]; // URLs de episodios

  url: string;

  created: string;  // Fecha de creación

}


/**

 * Interfaz para la respuesta completa de personajes

 */

export interface CharacterResponse {

  info: Info;

  results: Character[];

}


🎯 Paso 5: Consumir APIs en el Componente Principal

App.tsx completo:

typescript

import React, { useEffect, useState } from 'react';

import { SafeAreaView, View, Text, ActivityIndicator, StyleSheet } from 'react-native';

import { getApiInfo } from './src/api/RickAndMortyApi';

import { ApiInfoResponse } from './src/api/entities/ApiInfoResponse';

import { getCharacters } from './src/api/RickAndMortyApiAlt';

import { CharacterResponse } from './src/api/entities/CharacterResponse';


function App(): JSX.Element {

  const [apiInfo, setApiInfo] = useState<ApiInfoResponse | null>(null);

  const [characters, setCharacters] = useState<CharacterResponse | null>(null);

  const [loading, setLoading] = useState<boolean>(true);

  const [error, setError] = useState<string | null>(null);


  // Método 1: Usando Fetch con .then() y .catch()

  useEffect(() => {

    getApiInfo()

      .then(response => response.json())

      .then((data: ApiInfoResponse) => {

        console.log('ApiInfo:', JSON.stringify(data));

        setApiInfo(data);

      })

      .catch((error: Error) => {

        console.error('Error fetching API info:', error);

        setError('Error al obtener información de la API');

      });

  }, []);


  // Método 2: Usando Axios con async/await

  useEffect(() => {

    const fetchCharacters = async () => {

      try {

        setLoading(true);

        const response = await getCharacters();

        console.log('Characters Info:', JSON.stringify(response.info));

        setCharacters(response);

      } catch (error) {

        console.error('Error fetching characters:', error);

        setError('Error al obtener personajes');

      } finally {

        setLoading(false);

      }

    };


    fetchCharacters();

  }, []);


  if (loading) {

    return (

      <SafeAreaView style={styles.container}>

        <ActivityIndicator size="large" color="#0000ff" />

        <Text>Cargando datos...</Text>

      </SafeAreaView>

    );

  }


  if (error) {

    return (

      <SafeAreaView style={styles.container}>

        <Text style={styles.errorText}>{error}</Text>

      </SafeAreaView>

    );

  }


  return (

    <SafeAreaView style={styles.container}>

      <View style={styles.section}>

        <Text style={styles.title}>Información de la API</Text>

        {apiInfo && (

          <>

            <Text>Personajes: {apiInfo.characters}</Text>

            <Text>Locaciones: {apiInfo.locations}</Text>

            <Text>Episodios: {apiInfo.episodes}</Text>

          </>

        )}

      </View>


      <View style={styles.section}>

        <Text style={styles.title}>Personajes</Text>

        {characters && (

          <>

            <Text>Total: {characters.info.count} personajes</Text>

            <Text>Páginas: {characters.info.pages}</Text>

            <Text>Personajes en esta página: {characters.results.length}</Text>

            

            {/* Mostrar primeros 3 personajes como ejemplo */}

            {characters.results.slice(0, 3).map(character => (

              <View key={character.id} style={styles.characterCard}>

                <Text style={styles.characterName}>{character.name}</Text>

                <Text>Estado: {character.status}</Text>

                <Text>Especie: {character.species}</Text>

              </View>

            ))}

          </>

        )}

      </View>

    </SafeAreaView>

  );

}


const styles = StyleSheet.create({

  container: {

    flex: 1,

    padding: 16,

    backgroundColor: '#f5f5f5',

  },

  section: {

    backgroundColor: 'white',

    padding: 16,

    marginBottom: 16,

    borderRadius: 8,

    shadowColor: '#000',

    shadowOffset: { width: 0, height: 2 },

    shadowOpacity: 0.1,

    shadowRadius: 4,

    elevation: 3,

  },

  title: {

    fontSize: 18,

    fontWeight: 'bold',

    marginBottom: 12,

    color: '#333',

  },

  characterCard: {

    backgroundColor: '#f9f9f9',

    padding: 12,

    marginBottom: 8,

    borderRadius: 6,

    borderWidth: 1,

    borderColor: '#e0e0e0',

  },

  characterName: {

    fontSize: 16,

    fontWeight: '600',

    color: '#2196F3',

  },

  errorText: {

    color: 'red',

    fontSize: 16,

    textAlign: 'center',

  },

});


export default App;


🔍 Paso 6: Comparación Fetch vs Axios

Fetch (Nativo):

typescript

// Ventajas:

// ✅ Viene incluido en React Native

// ✅ No requiere dependencias adicionales

// ✅ API estándar de JavaScript


// Desventajas:

// ❌ Necesita conversión manual a JSON

// ❌ Manejo de errores menos intuitivo

// ❌ No tiene cancelación de requests por defecto


const response = await fetch(url);

const data = await response.json();

Axios (Librería):

typescript

// Ventajas:

// ✅ Conversión automática a JSON

// ✅ Manejo de errores más robusto

// ✅ Cancelación de requests

// ✅ Interceptores para headers globales

// ✅ Transformadores de request/response


// Desventajas:

// ❌ Dependencia externa

// ❌ Aumenta tamaño del bundle


const response = await axios.get(url);

const data = response.data;


🛡️ Paso 7: Manejo de Errores Avanzado

RickAndMortyApiAlt.ts con manejo de errores mejorado:

typescript

import axios, { AxiosError } from 'axios';


// Interceptor para agregar headers globales

apiClient.interceptors.request.use(

  (config) => {

    // Podemos agregar tokens de autenticación aquí

    // config.headers.Authorization = `Bearer ${token}`;

    return config;

  },

  (error) => {

    return Promise.reject(error);

  }

);


// Interceptor para respuestas

apiClient.interceptors.response.use(

  (response) => response,

  (error: AxiosError) => {

    if (error.response) {

      // El servidor respondió con un código de error

      switch (error.response.status) {

        case 401:

          console.error('No autorizado - Token expirado');

          break;

        case 403:

          console.error('Prohibido - Sin permisos');

          break;

        case 404:

          console.error('Recurso no encontrado');

          break;

        case 500:

          console.error('Error interno del servidor');

          break;

        default:

          console.error(`Error ${error.response.status}:`, error.message);

      }

    } else if (error.request) {

      // La petición fue hecha pero no hubo respuesta

      console.error('Sin respuesta del servidor - Revisa tu conexión');

    } else {

      // Error al configurar la petición

      console.error('Error en la configuración:', error.message);

    }

    

    return Promise.reject(error);

  }

);


📱 Paso 8: Custom Hook para Fetching de Datos

useCharacters.ts:

typescript

import { useState, useEffect, useCallback } from 'react';

import { getCharacters } from '../api/RickAndMortyApiAlt';

import { CharacterResponse } from '../api/entities/CharacterResponse';


export const useCharacters = (initialPage: number = 1) => {

  const [data, setData] = useState<CharacterResponse | null>(null);

  const [loading, setLoading] = useState<boolean>(false);

  const [error, setError] = useState<string | null>(null);

  const [page, setPage] = useState<number>(initialPage);


  const fetchCharacters = useCallback(async (pageNum: number) => {

    try {

      setLoading(true);

      setError(null);

      const response = await getCharacters(pageNum);

      setData(response);

    } catch (err) {

      setError(err instanceof Error ? err.message : 'Error desconocido');

    } finally {

      setLoading(false);

    }

  }, []);


  useEffect(() => {

    fetchCharacters(page);

  }, [page, fetchCharacters]);


  const nextPage = () => {

    if (data?.info.next) {

      setPage(prev => prev + 1);

    }

  };


  const prevPage = () => {

    if (data?.info.prev) {

      setPage(prev => prev - 1);

    }

  };


  return {

    data,

    loading,

    error,

    page,

    nextPage,

    prevPage,

    refetch: () => fetchCharacters(page),

  };

};

Uso en componente:

typescript

const CharacterList = () => {

  const { data, loading, error, page, nextPage, prevPage } = useCharacters();


  if (loading) return <Text>Cargando...</Text>;

  if (error) return <Text>Error: {error}</Text>;


  return (

    <View>

      <Text>Página {page} de {data?.info.pages}</Text>

      <Button title="Anterior" onPress={prevPage} disabled={!data?.info.prev} />

      <Button title="Siguiente" onPress={nextPage} disabled={!data?.info.next} />

      

      {data?.results.map(character => (

        <Text key={character.id}>{character.name}</Text>

      ))}

    </View>

  );

};


💡 Mejores Prácticas

1. Separación de responsabilidades:

  • ✅ APIs en archivos separados

  • ✅ Interfaces TypeScript para tipos de datos

  • ✅ Custom hooks para lógica reutilizable

2. Manejo de estados:

typescript

const [data, setData] = useState<T | null>(null);

const [loading, setLoading] = useState<boolean>(true);

const [error, setError] = useState<string | null>(null);

3. Limpieza de efectos:

typescript

useEffect(() => {

  let isMounted = true;

  const controller = new AbortController();


  const fetchData = async () => {

    try {

      const response = await fetch(url, { 

        signal: controller.signal 

      });

      if (isMounted) {

        setData(await response.json());

      }

    } catch (err) {

      if (isMounted && err.name !== 'AbortError') {

        setError(err.message);

      }

    }

  };


  fetchData();


  return () => {

    isMounted = false;

    controller.abort();

  };

}, []);

4. Variables de entorno:

typescript

// .env

API_URL=https://rickandmortyapi.com/api


// En el código

const API_URL = process.env.API_URL;


🎓 Resumen

Lo que aprendiste:

✅ Consumir APIs REST con Fetch (nativo)
✅ Consumir APIs REST con Axios (librería externa)
✅ Definir interfaces TypeScript para respuestas
✅ Manejar estados de carga y error
✅ Implementar paginación
✅ Crear custom hooks para reutilizar lógica
✅ Manejo de errores robusto

Siguientes pasos:

  1. Mostrar datos en una lista con FlatList

  2. Implementar búsqueda en tiempo real

  3. Cachear respuestas para mejor performance

  4. Agregar pull-to-refresh

  5. Implementar infinite scroll


📚 Recursos Adicionales

Documentación oficial:

Herramientas útiles:

¡Ahora estás listo para crear aplicaciones que consumen datos de APIs reales!


Comentarios

Entradas más populares de este blog

17. Estructura de una aplicación de React Native, una vista mas profunda

Guía Paso a Paso para Entender React Native (antes del Tutorial)

12. Configuracion del entorno de desarrollo en Windows