Travailler avec des API REST dans React Native

Travailler avec des API REST dans React Native

Travailler avec des API REST dans React Native

Quand on commence à développer une application mobile avec React Native, on découvre très vite que l’interface ne suffit pas. Une application utile, vivante et vraiment moderne doit parler à un backend, récupérer des données, en envoyer, modifier des ressources, synchroniser l’état local avec le serveur, gérer les erreurs réseau, afficher des messages intelligents, et parfois même continuer à fonctionner quand la connexion devient capricieuse. C’est précisément là que les API REST deviennent le cœur du projet. Elles font le lien entre l’expérience utilisateur et la logique métier, et dans une application mobile, ce lien doit être robuste, clair et facile à maintenir.

Travailler avec des API REST dans React Native, ce n’est pas seulement “faire des requêtes HTTP”. C’est apprendre à construire une architecture où les appels réseau ne se dispersent pas dans tous les composants, où les données sont récupérées de manière prévisible, où les erreurs sont traitées avec soin, et où chaque écran reste lisible même quand le projet grandit. Beaucoup de débutants écrivent d’abord le code directement dans leurs composants, puis se retrouvent quelques semaines plus tard avec des dizaines de fichiers difficiles à suivre. À l’inverse, une bonne organisation dès le départ permet de gagner du temps, d’éviter les bugs et de rendre l’application bien plus agréable à faire évoluer.

Dans cet article, nous allons voir, pas à pas, comment consommer une API REST dans React Native, comment structurer proprement vos requêtes, comment utiliser fetch ou Axios, comment gérer l’authentification, les erreurs, le chargement, la pagination, les opérations CRUD, et comment construire une base solide pour des projets réels. Nous allons aussi ajouter cette petite touche humaine qui manque souvent aux tutoriels trop mécaniques, parce qu’au fond, développer une app, c’est aussi apprendre à prévoir les imprévus, à simplifier ce qui paraît compliqué, et à garder une certaine élégance dans le code.

Comprendre ce qu’est une API REST

Une API REST, ou plus précisément une API qui suit les principes REST, est une interface qui permet à une application de communiquer avec un serveur en utilisant des requêtes HTTP standard. Vous avez probablement déjà rencontré ces verbes : GET pour lire des données, POST pour créer, PUT ou PATCH pour modifier, et DELETE pour supprimer. Ce modèle est devenu très populaire parce qu’il est simple à comprendre, compatible avec presque tous les langages, et particulièrement adapté aux applications web et mobiles.

Dans React Native, une API REST peut servir à charger une liste de produits, connecter un utilisateur, récupérer son profil, afficher ses notifications, envoyer un formulaire, ou encore synchroniser les favoris d’un compte. Le mobile devient alors une fenêtre dynamique sur un système plus vaste. C’est pour cela qu’il faut penser l’intégration API dès la conception de l’application, et pas comme une simple étape finale ajoutée à la hâte.

Une chose importante à retenir est que l’API REST n’est pas React Native lui-même. React Native fournit les outils pour construire l’interface utilisateur mobile, mais la logique d’échange avec le serveur reste à votre charge. C’est vous qui décidez comment stocker les tokens, comment organiser les services réseau, comment réagir quand le serveur répond lentement, et comment afficher des états utiles à l’utilisateur. Cette liberté est puissante, mais elle demande un minimum de discipline.

Pourquoi React Native et les API REST forment un duo si courant

React Native a été conçu pour permettre de créer des applications mobiles iOS et Android avec une seule base de code JavaScript ou TypeScript. De son côté, une API REST permet de centraliser les données et les règles métier côté serveur. Ensemble, ces deux mondes offrent une architecture très efficace. Vous construisez une interface mobile souple, et vous la connectez à un backend qui expose des données propres, structurées et réutilisables.

C’est un duo presque naturel. Si votre application affiche des publications, des messages, des réservations, des commandes ou des statistiques, ces informations viennent souvent d’un serveur. L’application mobile n’est pas censée tout contenir localement. Elle doit plutôt récupérer les données au bon moment, les présenter de manière fluide, puis envoyer les modifications nécessaires. C’est là que React Native montre toute sa valeur : il rend l’expérience utilisateur agréable, tandis que l’API fournit le contenu.

Dans la pratique, ce duo devient encore plus important dès que l’application doit gérer plusieurs écrans, plusieurs utilisateurs, ou des données qui évoluent souvent. Un bon design API permet de simplifier l’interface. Un bon design React Native permet de rendre la réponse de l’API lisible et rapide à l’écran. Quand les deux sont bien pensés, l’application respire mieux.

Première requête avec fetch

React Native fournit nativement l’API fetch, ce qui permet de faire des requêtes HTTP sans installer de bibliothèque supplémentaire. Pour un petit projet ou pour bien comprendre les bases, c’est une excellente manière de commencer. Voici un exemple simple de récupération de données depuis une API publique :

import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, ActivityIndicator, StyleSheet } from 'react-native';

export default function UsersScreen() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  const loadUsers = async () => {
    try {
      setLoading(true);
      setError('');

      const response = await fetch('https://jsonplaceholder.typicode.com/users');

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

      const data = await response.json();
      setUsers(data);
    } catch (err) {
      setError(err.message || 'Une erreur est survenue');
    } finally {
      setLoading(false);
    }
  };

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

  if (loading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" />
        <Text>Chargement des utilisateurs...</Text>
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.center}>
        <Text style={styles.error}>{error}</Text>
      </View>
    );
  }

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

const styles = StyleSheet.create({
  center: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  card: {
    padding: 16,
    marginHorizontal: 16,
    marginVertical: 8,
    backgroundColor: '#fff',
    borderRadius: 12,
    elevation: 2,
  },
  name: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 4,
  },
  error: {
    color: 'red',
    fontSize: 16,
  },
});

Cet exemple est volontairement simple, mais il montre déjà plusieurs choses essentielles. On initialise l’état de chargement, on récupère les données avec fetch, on vérifie la réponse du serveur, on traite l’erreur dans un catch, puis on affiche un indicateur pendant l’attente. C’est une base saine, et dans beaucoup de cas, cela suffit pour démarrer correctement.

Le principal avantage de fetch, c’est qu’il est intégré. Le principal inconvénient, c’est qu’il demande un peu plus de travail manuel quand le projet se complexifie. Vous devez gérer vous-même certains détails, comme les transformations de réponse, les en-têtes, les délais, ou encore l’organisation du code réseau. C’est pour cela que beaucoup d’équipes choisissent ensuite Axios.

Utiliser Axios pour simplifier les requêtes

Axios est une bibliothèque très populaire pour faire des requêtes HTTP. Elle apporte une syntaxe agréable, une meilleure ergonomie pour traiter les réponses JSON, un système d’intercepteurs, une gestion plus simple des configurations globales et une API souvent plus lisible que fetch. Dans un projet React Native un peu sérieux, Axios devient vite un allié précieux.

Voici un exemple d’installation :

npm install axios

Puis un exemple d’utilisation :

import axios from 'axios';
import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, ActivityIndicator, StyleSheet } from 'react-native';

const api = axios.create({
  baseURL: 'https://jsonplaceholder.typicode.com',
  timeout: 10000,
});

export default function PostsScreen() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  const loadPosts = async () => {
    try {
      setLoading(true);
      setError('');

      const response = await api.get('/posts');
      setPosts(response.data);
    } catch (err) {
      setError(err.response?.data?.message || err.message || 'Erreur inconnue');
    } finally {
      setLoading(false);
    }
  };

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

  if (loading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" />
        <Text>Chargement des articles...</Text>
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.center}>
        <Text style={styles.error}>{error}</Text>
      </View>
    );
  }

  return (
    <FlatList
      data={posts}
      keyExtractor={(item) => item.id.toString()}
      renderItem={({ item }) => (
        <View style={styles.card}>
          <Text style={styles.title}>{item.title}</Text>
          <Text numberOfLines={3}>{item.body}</Text>
        </View>
      )}
    />
  );
}

const styles = StyleSheet.create({
  center: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  card: {
    padding: 16,
    marginHorizontal: 16,
    marginVertical: 8,
    backgroundColor: '#fff',
    borderRadius: 12,
    elevation: 2,
  },
  title: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 8,
  },
  error: {
    color: 'red',
    fontSize: 16,
  },
});

Axios devient particulièrement utile quand vous devez gérer plusieurs appels dans toute l’application. Vous pouvez centraliser la configuration, définir une base URL, ajouter automatiquement un token d’authentification, ou standardiser les erreurs. Cela évite la répétition et réduit les incohérences. Quand le code métier grandit, cette clarté fait une vraie différence.

Organiser les appels dans un service API

L’un des pièges les plus fréquents consiste à appeler directement l’API dans les composants d’interface. Au début, cela peut sembler rapide. En réalité, cette approche crée souvent un code mélangé, difficile à relire et encore plus difficile à tester. Une meilleure approche consiste à centraliser les appels réseau dans un dossier services ou api.

Par exemple, vous pouvez créer un fichier apiClient.js :

import axios from 'axios';

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

export default apiClient;

Puis un service userService.js :

import apiClient from './apiClient';

export const getUsers = async () => {
  const response = await apiClient.get('/users');
  return response.data;
};

export const getUserById = async (id) => {
  const response = await apiClient.get(`/users/${id}`);
  return response.data;
};

export const createUser = async (userData) => {
  const response = await apiClient.post('/users', userData);
  return response.data;
};

export const updateUser = async (id, userData) => {
  const response = await apiClient.put(`/users/${id}`, userData);
  return response.data;
};

export const deleteUser = async (id) => {
  const response = await apiClient.delete(`/users/${id}`);
  return response.data;
};

Puis dans votre composant, vous appelez simplement la fonction voulue :

import React, { useEffect, useState } from 'react';
import { View, Text, FlatList } from 'react-native';
import { getUsers } from './services/userService';

export default function UsersList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const fetchData = async () => {
      const data = await getUsers();
      setUsers(data);
    };

    fetchData();
  }, []);

  return (
    <FlatList
      data={users}
      keyExtractor={(item) => item.id.toString()}
      renderItem={({ item }) => <Text>{item.name}</Text>}
    />
  );
}

Cette séparation change tout. Le composant se concentre sur l’affichage, le service se concentre sur les données, et la logique réseau devient plus facile à modifier. Le jour où l’API change, vous aurez moins de fichiers à toucher. Le jour où vous voudrez tester vos appels, vous serez content d’avoir fait ce choix.

Gérer les états de chargement, succès et erreur

Une application mobile qui consomme une API doit toujours penser au temps d’attente. Sur un réseau rapide, le chargement semble invisible. Sur un réseau instable, il devient soudain très présent. Et sur mobile, cette variabilité est la norme. C’est pourquoi il faut traiter proprement les états loading, error et success.

Un utilisateur n’aime pas seulement voir un écran vide. Il veut savoir que quelque chose se passe. Il veut aussi comprendre ce qui ne va pas si la requête échoue. Un simple message comme “Erreur” ne suffit pas. Il vaut mieux expliquer ce que l’utilisateur peut faire : réessayer, vérifier sa connexion, ou revenir plus tard.

Voici un exemple de composant qui gère correctement ces états :

import React, { useState } from 'react';
import { View, Text, Button, ActivityIndicator } from 'react-native';
import axios from 'axios';

export default function LoadDataButton() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const fetchData = async () => {
    try {
      setLoading(true);
      setError('');
      const response = await axios.get('https://jsonplaceholder.typicode.com/todos/1');
      setData(response.data);
    } catch (e) {
      setError('Impossible de charger les données. Vérifiez votre connexion.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <View style={{ padding: 20 }}>
      <Button title="Charger les données" onPress={fetchData} />

      {loading && (
        <View style={{ marginTop: 20 }}>
          <ActivityIndicator />
          <Text>Veuillez patienter...</Text>
        </View>
      )}

      {error ? <Text style={{ color: 'red', marginTop: 20 }}>{error}</Text> : null}

      {data ? (
        <View style={{ marginTop: 20 }}>
          <Text>ID : {data.id}</Text>
          <Text>Tâche : {data.title}</Text>
        </View>
      ) : null}
    </View>
  );
}

Le but n’est pas seulement de “faire marcher l’API”. Le but est de faire ressentir à l’utilisateur que l’application reste sous contrôle, même quand le réseau ne coopère pas.

Ajouter l’authentification avec JWT

Dans beaucoup d’applications mobiles, l’accès à certaines ressources nécessite une authentification. Le backend renvoie souvent un token JWT après la connexion, et ce token doit être envoyé dans les requêtes suivantes via l’en-tête Authorization. C’est l’un des scénarios les plus courants dans les projets réels.

Voici un exemple de connexion :

import apiClient from './apiClient';

export const login = async (email, password) => {
  const response = await apiClient.post('/auth/login', {
    email,
    password,
  });

  return response.data;
};

Supposons que la réponse contienne un token :

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
  "user": {
    "id": 1,
    "name": "Fatima",
    "email": "fatima@example.com"
  }
}

Ensuite, vous pouvez configurer Axios pour ajouter automatiquement ce token :

import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
});

apiClient.interceptors.request.use(
  async (config) => {
    const token = await AsyncStorage.getItem('accessToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

export default apiClient;

Et lors de la connexion :

import AsyncStorage from '@react-native-async-storage/async-storage';
import { login } from './services/authService';

const handleLogin = async () => {
  try {
    const result = await login(email, password);
    await AsyncStorage.setItem('accessToken', result.accessToken);
    await AsyncStorage.setItem('user', JSON.stringify(result.user));
  } catch (error) {
    console.log('Erreur de connexion', error);
  }
};

L’authentification est une étape sensible. Le token doit être stocké proprement, la session doit être gérée avec soin, et les erreurs liées à l’expiration doivent être anticipées. Quand une application est bien pensée, l’utilisateur n’a pas l’impression de “subir” la sécurité. Il la vit naturellement, sans friction inutile.

Faire du CRUD avec une API REST

CRUD signifie Create, Read, Update, Delete. C’est la base de presque toutes les applications métiers. Dans React Native, cela correspond à afficher des ressources, en créer, les modifier et les supprimer. C’est souvent à partir de ce moment-là qu’un projet prend une vraie forme.

Prenons un exemple simple d’API de tâches :

import apiClient from './apiClient';

export const getTasks = async () => {
  const response = await apiClient.get('/tasks');
  return response.data;
};

export const addTask = async (task) => {
  const response = await apiClient.post('/tasks', task);
  return response.data;
};

export const editTask = async (id, task) => {
  const response = await apiClient.patch(`/tasks/${id}`, task);
  return response.data;
};

export const removeTask = async (id) => {
  const response = await apiClient.delete(`/tasks/${id}`);
  return response.data;
};

Et un écran simple pour afficher les tâches :

import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, Pressable, Alert } from 'react-native';
import { getTasks, removeTask } from './services/taskService';

export default function TasksScreen() {
  const [tasks, setTasks] = useState([]);

  const loadTasks = async () => {
    const data = await getTasks();
    setTasks(data);
  };

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

  const handleDelete = async (id) => {
    Alert.alert(
      'Suppression',
      'Voulez-vous vraiment supprimer cette tâche ?',
      [
        { text: 'Annuler', style: 'cancel' },
        {
          text: 'Supprimer',
          style: 'destructive',
          onPress: async () => {
            await removeTask(id);
            setTasks((current) => current.filter((task) => task.id !== id));
          },
        },
      ]
    );
  };

  return (
    <FlatList
      data={tasks}
      keyExtractor={(item) => item.id.toString()}
      renderItem={({ item }) => (
        <View style={{ padding: 16, borderBottomWidth: 1, borderColor: '#eee' }}>
          <Text style={{ fontSize: 16, fontWeight: '600' }}>{item.title}</Text>
          <Pressable onPress={() => handleDelete(item.id)}>
            <Text style={{ color: 'red', marginTop: 8 }}>Supprimer</Text>
          </Pressable>
        </View>
      )}
    />
  );
}

Ce type de logique est très courant, mais il mérite d’être traité avec attention. Lorsque vous supprimez un élément, il faut idéalement mettre à jour l’interface immédiatement, tout en restant prêt à gérer un éventuel échec côté serveur. L’expérience utilisateur ne doit jamais donner l’impression que l’application hésite entre deux réalités.

Gérer la pagination

Dès que vos données deviennent nombreuses, vous devrez faire face à la pagination. Charger cent, mille ou dix mille éléments d’un coup n’est ni élégant ni efficace sur mobile. La pagination permet de demander les données petit à petit, ce qui améliore les performances, réduit la consommation réseau et évite d’alourdir l’interface.

Supposons que votre API accepte page et limit :

export const getProducts = async (page = 1, limit = 20) => {
  const response = await apiClient.get('/products', {
    params: { page, limit },
  });
  return response.data;
};

Puis dans un écran :

import React, { useEffect, useState } from 'react';
import { FlatList, Text, ActivityIndicator, View } from 'react-native';
import { getProducts } from './services/productService';

export default function ProductsScreen() {
  const [products, setProducts] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [loadingMore, setLoadingMore] = useState(false);
  const [hasMore, setHasMore] = useState(true);

  const loadProducts = async (currentPage = 1) => {
    try {
      if (currentPage === 1) setLoading(true);
      else setLoadingMore(true);

      const result = await getProducts(currentPage, 20);

      setProducts((prev) =>
        currentPage === 1 ? result.items : [...prev, ...result.items]
      );
      setHasMore(result.items.length > 0);
      setPage(currentPage);
    } finally {
      setLoading(false);
      setLoadingMore(false);
    }
  };

  useEffect(() => {
    loadProducts(1);
  }, []);

  const loadMore = () => {
    if (!loadingMore && hasMore) {
      loadProducts(page + 1);
    }
  };

  if (loading) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <ActivityIndicator size="large" />
      </View>
    );
  }

  return (
    <FlatList
      data={products}
      keyExtractor={(item) => item.id.toString()}
      renderItem={({ item }) => <Text style={{ padding: 16 }}>{item.name}</Text>}
      onEndReached={loadMore}
      onEndReachedThreshold={0.5}
      ListFooterComponent={loadingMore ? <ActivityIndicator /> : null}
    />
  );
}

La pagination est presque toujours préférable à un gros chargement initial. Sur mobile, le confort ressenti par l’utilisateur dépend beaucoup de cette discipline invisible. Une app fluide est souvent une app qui ne force pas tout à la fois.

Utiliser des hooks personnalisés pour mieux organiser le code

Les hooks personnalisés sont extrêmement utiles pour réutiliser la logique liée aux API. Au lieu d’écrire le même schéma loading / error / fetch / setState dans plusieurs écrans, vous pouvez centraliser cette logique dans un hook. Cela rend votre code plus propre, plus lisible et plus facile à maintenir.

Voici un exemple de hook générique :

import { useState, useCallback } from 'react';

export function useApi(requestFn) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const execute = useCallback(async (...args) => {
    try {
      setLoading(true);
      setError('');
      const result = await requestFn(...args);
      setData(result);
      return result;
    } catch (err) {
      setError(err.message || 'Erreur API');
      throw err;
    } finally {
      setLoading(false);
    }
  }, [requestFn]);

  return {
    data,
    loading,
    error,
    execute,
    setData,
  };
}

Utilisation :

import React, { useEffect } from 'react';
import { View, Text, ActivityIndicator } from 'react-native';
import { useApi } from './hooks/useApi';
import { getUsers } from './services/userService';

export default function UsersHookScreen() {
  const { data, loading, error, execute } = useApi(getUsers);

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

  if (loading) return <ActivityIndicator />;
  if (error) return <Text>{error}</Text>;

  return (
    <View>
      {data?.map((user) => (
        <Text key={user.id}>{user.name}</Text>
      ))}
    </View>
  );
}

Ce genre d’abstraction peut sembler un peu “technique” au départ, mais il devient vite très pratique. Le projet reste lisible, la logique réseau se répète moins, et vous gagnez en cohérence.

Gérer les erreurs intelligemment

Toutes les erreurs ne se ressemblent pas. Une erreur réseau n’est pas la même chose qu’une erreur de validation, qu’une erreur 401, qu’une réponse 500 ou qu’un timeout. Une bonne application doit distinguer ces cas autant que possible, afin de fournir le bon message au bon moment.

Exemple d’un traitement plus précis :

try {
  const response = await apiClient.post('/login', { email, password });
  return response.data;
} catch (error) {
  if (error.response) {
    const status = error.response.status;

    if (status === 400) {
      throw new Error('Données invalides.');
    }

    if (status === 401) {
      throw new Error('Email ou mot de passe incorrect.');
    }

    if (status === 500) {
      throw new Error('Erreur serveur. Réessayez plus tard.');
    }

    throw new Error('Erreur inattendue.');
  }

  if (error.request) {
    throw new Error('Impossible de contacter le serveur.');
  }

  throw new Error('Une erreur est survenue.');
}

Ce niveau de précision améliore énormément la qualité perçue de l’application. L’utilisateur n’a pas besoin d’un discours technique, mais il a besoin d’un message juste. Une bonne erreur n’est pas une erreur “complexe”, c’est une erreur compréhensible.

Rafraîchir les données et recharger au bon moment

Dans une application mobile, les données ne restent pas toujours valides très longtemps. Un utilisateur peut revenir sur un écran après plusieurs minutes ou plusieurs heures. Il est donc utile de prévoir un mécanisme de rafraîchissement manuel ou automatique.

Avec React Native, vous pouvez par exemple proposer un pull-to-refresh :

import React, { useState, useEffect } from 'react';
import { FlatList, RefreshControl, Text } from 'react-native';
import { getTasks } from './services/taskService';

export default function RefreshTasksScreen() {
  const [tasks, setTasks] = useState([]);
  const [refreshing, setRefreshing] = useState(false);

  const loadTasks = async () => {
    const data = await getTasks();
    setTasks(data);
  };

  const onRefresh = async () => {
    setRefreshing(true);
    await loadTasks();
    setRefreshing(false);
  };

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

  return (
    <FlatList
      data={tasks}
      keyExtractor={(item) => item.id.toString()}
      renderItem={({ item }) => <Text style={{ padding: 16 }}>{item.title}</Text>}
      refreshControl={
        <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
      }
    />
  );
}

Ce petit geste est très apprécié sur mobile. Il donne à l’utilisateur le sentiment de garder le contrôle, ce qui compte beaucoup dans une interface tactile.

Sécuriser les appels API

La sécurité mérite une vraie réflexion. Il ne suffit pas de “mettre un token quelque part”. Il faut penser à la manière dont ce token est stocké, envoyé, renouvelé et supprimé. Sur mobile, on évite en général de stocker des données sensibles dans des endroits non adaptés. Il faut aussi s’assurer que les endpoints utilisent HTTPS, que les données sensibles ne sont pas exposées inutilement, et que le backend valide tout ce qui arrive.

Quelques bonnes pratiques utiles :

Le token d’authentification doit être stocké avec soin.
Les requêtes sensibles doivent passer par HTTPS.
Le backend doit toujours valider les données, même si le client semble propre.
Les messages d’erreur ne doivent pas révéler des informations inutiles.
Les secrets d’API ne doivent jamais être codés en dur dans le code source public.

Vous pouvez aussi utiliser des variables d’environnement pour éviter d’éparpiller les URLs et les clés dans le projet. En React Native, cela se fait souvent avec des outils dédiés selon la stack utilisée. L’idée reste la même : séparer la configuration du code métier.

Tester vos appels API

Les tests sont souvent négligés sur les premières versions d’un projet, puis deviennent soudain indispensables. Tester les appels API, c’est s’assurer que le comportement reste stable même quand le backend change ou que de nouvelles fonctionnalités s’ajoutent.

On peut tester au moins trois niveaux :

Les fonctions de service, pour vérifier qu’elles appellent la bonne URL avec les bons paramètres.
Les hooks, pour vérifier la gestion des états de chargement et d’erreur.
Les composants, pour vérifier que l’interface réagit correctement à une réponse API.

Par exemple, avec Jest, vous pouvez mocker Axios :

import apiClient from '../services/apiClient';
import { getUsers } from '../services/userService';

jest.mock('../services/apiClient');

test('getUsers retourne les utilisateurs', async () => {
  apiClient.get.mockResolvedValue({
    data: [{ id: 1, name: 'Amina' }],
  });

  const users = await getUsers();

  expect(users).toEqual([{ id: 1, name: 'Amina' }]);
});

Les tests ne sont pas juste un luxe pour les grandes équipes. Ils sont un filet de sécurité. Et dans une application qui parle à une API, ce filet devient vite très utile.

Optimiser les performances

Une API REST bien utilisée doit aussi être pensée pour la performance. Sur mobile, chaque requête compte. Chaque écran doit être raisonnable dans sa consommation réseau et sa fréquence de mise à jour. Il faut éviter les appels inutiles, mémoriser certains résultats quand c’est pertinent, et ne pas recharger tout ce qui pourrait être réutilisé.

Quelques gestes simples améliorent beaucoup les choses :

Éviter de déclencher des requêtes à chaque re-render.
Utiliser un cache si les données sont stables.
Préférer la pagination aux chargements massifs.
Ne pas afficher plus de données que nécessaire.
Annuler les requêtes devenues inutiles lorsque l’écran change.

Même si l’optimisation avancée dépend du projet, ces bases sont déjà très efficaces. Une application rapide n’est pas seulement une application “bien codée”. C’est une application qui respecte le temps et l’attention de l’utilisateur.

Architecture recommandée pour un projet React Native avec API REST

Pour éviter le chaos, il est utile de structurer le projet dès le départ. Une structure simple pourrait ressembler à cela :

src/
  api/
    apiClient.js
  services/
    authService.js
    userService.js
    productService.js
  hooks/
    useApi.js
  screens/
    LoginScreen.js
    HomeScreen.js
    ProfileScreen.js
  components/
    Button.js
    Loader.js
    ErrorMessage.js
  utils/
    storage.js
    errors.js

Cette organisation n’est pas obligatoire, mais elle aide énormément. Les responsabilités sont mieux séparées. Le client API reste au même endroit. Les services réseau sont centralisés. Les composants visuels ne sont pas pollués par les détails HTTP. Et quand le projet grossit, ce genre de structure protège votre santé mentale autant que votre productivité.

Exemple complet : connexion et récupération du profil utilisateur

Voici un exemple plus concret, qui illustre un mini flux complet : connexion, sauvegarde du token, puis récupération du profil.

apiClient.js :

import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
});

apiClient.interceptors.request.use(async (config) => {
  const token = await AsyncStorage.getItem('accessToken');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

export default apiClient;

authService.js :

import apiClient from '../api/apiClient';

export const login = async (email, password) => {
  const response = await apiClient.post('/auth/login', { email, password });
  return response.data;
};

userService.js :

import apiClient from '../api/apiClient';

export const getProfile = async () => {
  const response = await apiClient.get('/me');
  return response.data;
};

LoginScreen.js :

import React, { useState } from 'react';
import { View, TextInput, Button, Text, Alert } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { login } from '../services/authService';

export default function LoginScreen() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleLogin = async () => {
    try {
      const result = await login(email, password);
      await AsyncStorage.setItem('accessToken', result.accessToken);
      Alert.alert('Succès', 'Connexion réussie');
    } catch (error) {
      Alert.alert('Erreur', 'Impossible de se connecter');
    }
  };

  return (
    <View style={{ padding: 20 }}>
      <TextInput
        placeholder="Email"
        value={email}
        onChangeText={setEmail}
        style={{ borderWidth: 1, marginBottom: 12, padding: 10 }}
      />
      <TextInput
        placeholder="Mot de passe"
        value={password}
        secureTextEntry
        onChangeText={setPassword}
        style={{ borderWidth: 1, marginBottom: 12, padding: 10 }}
      />
      <Button title="Se connecter" onPress={handleLogin} />
    </View>
  );
}

ProfileScreen.js :

import React, { useEffect, useState } from 'react';
import { View, Text, ActivityIndicator } from 'react-native';
import { getProfile } from '../services/userService';

export default function ProfileScreen() {
  const [profile, setProfile] = useState(null);

  useEffect(() => {
    const loadProfile = async () => {
      const data = await getProfile();
      setProfile(data);
    };

    loadProfile();
  }, []);

  if (!profile) {
    return <ActivityIndicator style={{ flex: 1 }} />;
  }

  return (
    <View style={{ padding: 20 }}>
      <Text>Nom : {profile.name}</Text>
      <Text>Email : {profile.email}</Text>
    </View>
  );
}

Ce petit flux est très représentatif des applications réelles. Il montre comment une API peut soutenir toute l’expérience utilisateur, depuis l’authentification jusqu’à l’affichage du compte.

Erreurs fréquentes à éviter

Beaucoup de problèmes liés aux API dans React Native viennent moins de la technologie elle-même que de quelques habitudes dangereuses. La première est de mélanger la logique réseau et la logique d’affichage. La deuxième est d’ignorer les cas d’erreur, en supposant que le serveur répondra toujours bien. La troisième est d’oublier que le mobile n’est pas un bureau avec une connexion stable et continue. La quatrième est de disperser les URLs et les headers dans plusieurs fichiers, ce qui rend la maintenance pénible.

Il faut aussi éviter de supposer que toutes les réponses API auront exactement la même forme. Un backend évolue. Une clé JSON peut changer. Un champ peut devenir optionnel. Une erreur peut apparaître là où tout semblait stable. Un bon client mobile doit être tolérant et prévoyant. C’est une forme de maturité technique, mais aussi une marque de respect pour l’utilisateur final.

Les bonnes habitudes qui font vraiment la différence

Une application mobile qui consomme une API REST devient meilleure dès qu’on adopte quelques habitudes simples. Définir une structure claire. Centraliser les appels réseau. Gérer les erreurs de façon explicite. Afficher le chargement. Protéger les données sensibles. Tester les services. Garder les composants légers. Éviter les duplications. Prévoir la pagination. Prévoir le refresh. Prévoir la déconnexion. Ce ne sont pas des détails. Ce sont les piliers d’une application qui tient dans la durée.

Et surtout, il faut garder une idée simple en tête : une API REST n’est pas seulement un moyen technique de récupérer des données. C’est la conversation entre votre application et le monde extérieur. Plus cette conversation est claire, plus l’expérience utilisateur sera fluide. Plus votre code sera organisé, plus cette conversation sera facile à maintenir. Et plus vous travaillez avec attention, plus le résultat donnera l’impression d’une application “naturelle”, comme si tout s’enchaînait sans effort.

Conclusion

Travailler avec des API REST dans React Native est une compétence essentielle pour créer des applications mobiles modernes, connectées et réellement utiles. Au début, tout peut sembler un peu abstrait : les requêtes HTTP, les réponses JSON, les états de chargement, les tokens, les erreurs, la pagination, les services, les hooks. Mais très vite, une logique se dessine. Vous commencez à voir qu’un bon projet ne repose pas seulement sur des écrans jolis, mais sur une architecture cohérente et respectueuse du flux de données.

Avec fetch, vous avez déjà de quoi démarrer. Avec Axios, vous gagnez en confort. Avec des services séparés, vous gagnez en organisation. Avec des hooks personnalisés, vous gagnez en réutilisabilité. Avec une bonne gestion des erreurs, vous gagnez en fiabilité. Avec une authentification bien pensée, vous gagnez en sécurité. Et avec une structure propre, vous gagnez surtout en sérénité. C’est souvent ce dernier point qui fait toute la différence sur le long terme.

Une application mobile n’est jamais vraiment “finie” au moment où elle sort. Elle évolue, elle se stabilise, elle reçoit des corrections, elle ajoute de nouveaux besoins. C’est pour cela qu’il faut construire ses intégrations API comme on construirait une maison solide : avec des fondations claires, des pièces bien séparées, et assez de souplesse pour s’adapter au futur. C’est exactement ce que React Native permet, à condition de l’accompagner d’une façon propre et réfléchie de travailler avec les API REST.

#React Native #API REST #fetch React Native #Axios React Native #authentification API #gestion des erreurs #mobile development #hooks React Native #JWT #pagination #CRUD mobile #best practices React Native

Abonnez-vous à notre newsletter

12k+

Abonnés

Hebdomadaire

Fréquence

Gratuit

Toujours