React Native course ยท Module 4: State and API data

Fetch API and Axios - Connecting to the Server

3 min read ยท
In this lesson4

Time to learn how to communicate with external APIs - the servers of Atlantis!

Fetch API - Built-in Solution

import { useState, useEffect } from 'react';

interface Creature {
  id: string;
  name: string;
  species: string;
  habitat: string;
}

function CreatureList() {
  const [creatures, setCreatures] = useState<Creature[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetchCreatures();
  }, []);

  const fetchCreatures = async () => {
    try {
      setIsLoading(true);
      const response = await fetch('https://api.atlantis.app/creatures');

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const data = await response.json();
      setCreatures(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    } finally {
      setIsLoading(false);
    }
  };

  // POST request
  const createCreature = async (creature: Omit<Creature, 'id'>) => {
    try {
      const response = await fetch('https://api.atlantis.app/creatures', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer token123',
        },
        body: JSON.stringify(creature),
      });

      if (!response.ok) {
        throw new Error('Failed to create creature');
      }

      const newCreature = await response.json();
      setCreatures(prev => [...prev, newCreature]);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Error');
    }
  };

  if (isLoading) return <ActivityIndicator size="large" />;
  if (error) return <Text style={styles.error}>{error}</Text>;

  return (
    <FlatList
      data={creatures}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View style={styles.card}>
          <Text style={styles.name}>{item.name}</Text>
          <Text>{item.species}</Text>
        </View>
      )}
    />
  );
}

Axios - Feature-rich Library

npx expo install axios
// api/client.ts
import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';

// Instance configuration
const apiClient = axios.create({
  baseURL: 'https://api.atlantis.app',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Interceptor - add token to every request
apiClient.interceptors.request.use(
  async (config) => {
    const token = await AsyncStorage.getItem('authToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// Interceptor - error handling
apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      // Token expired - log the user out
      await AsyncStorage.removeItem('authToken');
      // Redirect to login
    }
    return Promise.reject(error);
  }
);

export default apiClient;

API Service

// api/creatureService.ts
import apiClient from './client';

export interface Creature {
  id: string;
  name: string;
  species: string;
  habitat: string;
}

export const creatureService = {
  getAll: async (): Promise<Creature[]> => {
    const response = await apiClient.get('/creatures');
    return response.data;
  },

  getById: async (id: string): Promise<Creature> => {
    const response = await apiClient.get(`/creatures/${id}`);
    return response.data;
  },

  create: async (creature: Omit<Creature, 'id'>): Promise<Creature> => {
    const response = await apiClient.post('/creatures', creature);
    return response.data;
  },

  update: async (id: string, updates: Partial<Creature>): Promise<Creature> => {
    const response = await apiClient.patch(`/creatures/${id}`, updates);
    return response.data;
  },

  delete: async (id: string): Promise<void> => {
    await apiClient.delete(`/creatures/${id}`);
  },

  search: async (query: string): Promise<Creature[]> => {
    const response = await apiClient.get('/creatures/search', {
      params: { q: query },
    });
    return response.data;
  },
};

Usage in a Component

import { creatureService } from '@/api/creatureService';

function CreatureScreen() {
  const [creatures, setCreatures] = useState<Creature[]>([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    loadCreatures();
  }, []);

  const loadCreatures = async () => {
    try {
      setIsLoading(true);
      const data = await creatureService.getAll();
      setCreatures(data);
    } catch (error) {
      if (axios.isAxiosError(error)) {
        console.error('Axios error:', error.response?.data);
      }
    } finally {
      setIsLoading(false);
    }
  };

  const handleCreate = async () => {
    try {
      const newCreature = await creatureService.create({
        name: 'New Triton',
        species: 'Triton',
        habitat: 'The Depths',
      });
      setCreatures(prev => [...prev, newCreature]);
    } catch (error) {
      console.error('Create error:', error);
    }
  };

  return (
    // ... JSX
  );
}

Axios offers more features than fetch: interceptors, automatic transformations, request cancellation, and better error handling.

Code for this lesson: api/client.ts
import axios from 'axios';

// axios instance configuration
const apiClient = axios.create({
  baseURL: 'https://api.atlantis.app',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Simulated token retrieval (normally from AsyncStorage)
async function getAuthToken(): Promise<string | null> {
  return 'token-atlantis-123';
}

// TODO: add a request interceptor that:
//   - retrieves the token via getAuthToken()
//   - if the token exists, sets config.headers.Authorization = `Bearer ${token}`
//   - returns config
apiClient.interceptors.request.use(
  async (config) => {
    const token = await getAuthToken();
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// TODO: add a response interceptor that, on status 401,
//   logs a warning about token expiration
apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      console.warn('Token expired - log out the user');
    }
    return Promise.reject(error);
  }
);

export default apiClient;

Docs entries used in this lesson

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. How do you correctly fetch JSON data using fetch?

  2. 2. Which advantage does NOT apply to axios compared to fetch?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Vertical ordering

    Arrange fetch POST elements from outer to inner:

  • Code editor

    Complete the apiClient code with Bearer token.

Useful articles