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:
Mostrar datos en una lista con FlatList
Implementar búsqueda en tiempo real
Cachear respuestas para mejor performance
Agregar pull-to-refresh
Implementar infinite scroll
📚 Recursos Adicionales
Documentación oficial:
Herramientas útiles:
Postman - Para probar APIs
React Query - Para manejo de estado de servidor
¡Ahora estás listo para crear aplicaciones que consumen datos de APIs reales!
Comentarios
Publicar un comentario