Chapitre 1 : useApi

Le hook useApi simplifie les appels API avec gestion automatique de l'authentification JWT, du rafraîchissement de token et de la gestion des erreurs.

Configuration

L'API est configurée dans appConfig.js :

// src/appConfig.js
export const config = {
    api: {
        prefixUrl: import.meta.env.VITE_API_URL,
        timeout: 30000,
        debug: import.meta.env.DEV,
        paths: {
            login: "login",
            logout: "logout",
            refresh: "refresh"
        }
    }
};

Structure retournée

import { useApi } from '@cap-rel/smartcommon';

const api = useApi();
Méthode Description
api.user Objet utilisateur connecté
api.login(body) Connexion utilisateur
api.logout() Déconnexion
api.entities() Récupérer les entités disponibles
api.device(body) Enregistrer un appareil
api.public Instance ky pour requêtes publiques
api.private Instance ky pour requêtes authentifiées
api.get(url, options) Raccourci GET authentifié
api.post(url, options) Raccourci POST authentifié
api.put(url, options) Raccourci PUT authentifié
api.patch(url, options) Raccourci PATCH authentifié
api.del(url, options) Raccourci DELETE authentifié

Authentification

Login

import { useApi, useNavigation } from '@cap-rel/smartcommon';

function LoginPage() {
    const api = useApi();
    const nav = useNavigation();

    const handleLogin = async (email, password) => {
        try {
            const user = await api.login({
                login: email,
                password: password,
                rememberMe: true
            });

            console.log('Connecté :', user);
            nav.navigate('/');
        } catch (error) {
            console.error('Erreur de connexion:', error);
            alert('Identifiants incorrects');
        }
    };

    return (
        <form onSubmit={(e) => {
            e.preventDefault();
            const fd = new FormData(e.target);
            handleLogin(fd.get('email'), fd.get('password'));
        }}>
            <input name="email" type="email" placeholder="Email" />
            <input name="password" type="password" placeholder="Mot de passe" />
            <button type="submit">Connexion</button>
        </form>
    );
}

Logout

function LogoutButton() {
    const api = useApi();
    const nav = useNavigation();

    const handleLogout = async () => {
        await api.logout();
        nav.navigate('/login');
    };

    return (
        <button onClick={handleLogout}>Déconnexion</button>
    );
}

Accès à l'utilisateur connecté

useApi expose directement l'objet utilisateur :

const api = useApi();

if (api.user) {
    console.log('Connecté :', api.user.login);
    console.log('Token expire à :', api.user.tokenExpiry);
}

Requêtes publiques

Pour les endpoints sans authentification :

const api = useApi();

// GET
const info = await api.public.get('public/info').json();

// POST
const result = await api.public.post('public/contact', {
    json: { name: 'Jean', message: 'Bonjour' }
}).json();

Requêtes authentifiées

Pour les endpoints protégés par JWT :

const api = useApi();

// GET - Liste
const items = await api.private.get('items').json();

// GET - Détail
const item = await api.private.get(`items/${id}`).json();

// POST - Création
const newItem = await api.private.post('items', {
    json: { label: 'Nouveau produit', price: 29.99 }
}).json();

// PUT - Modification
const updated = await api.private.put(`items/${id}`, {
    json: { label: 'Produit modifié' }
}).json();

// DELETE - Suppression
await api.del(`items/${id}`);

Méthodes raccourcies

En plus des méthodes api.private.get(...).json(), useApi expose des raccourcis qui gèrent automatiquement le parsing JSON et les erreurs :

const api = useApi();

// GET - retourne directement les données parsées
const items = await api.get('items');

// POST
const created = await api.post('items', { json: { label: 'Nouveau' } });

// PUT
const updated = await api.put(`items/${id}`, { json: data });

// PATCH
const patched = await api.patch(`items/${id}`, { json: { status: 1 } });

// DELETE
await api.del(`items/${id}`);

Option raw

Pour récupérer des données binaires (images, fichiers Excel, PDF...) :

const response = await api.get(`temp-file/${token}/binary`, { raw: true });
const blob = await response.blob();

Option silent

Supprime la notification automatique en cas d'erreur :

const data = await api.get('optional-endpoint', { silent: true });

Exemple complet : CRUD

import { useEffect } from 'react';
import { Page, Block, List, ListItem, Button, Spinner } from '@cap-rel/smartcommon';
import { useApi, useStates, useNavigation } from '@cap-rel/smartcommon';

export const ProductsPage = () => {
    const api = useApi();
    const nav = useNavigation();

    const st = useStates({
        initialStates: {
            products: [],
            loading: true,
            error: null
        }
    });

    // Charger les produits
    useEffect(() => {
        loadProducts();
    }, []);

    const loadProducts = async () => {
        st.set('loading', true);
        st.set('error', null);

        try {
            const data = await api.private.get('products').json();
            st.set('products', data.products);
        } catch (err) {
            st.set('error', err.message);
        } finally {
            st.set('loading', false);
        }
    };

    // Supprimer un produit
    const deleteProduct = async (id) => {
        if (!confirm('Supprimer ce produit ?')) return;

        try {
            await api.del(`products/${id}`);
            // Retirer de la liste locale
            st.set('products', st.get('products').filter(p => p.id !== id));
        } catch (err) {
            alert('Erreur : ' + err.message);
        }
    };

    if (st.get('loading')) {
        return <Page title="Produits"><Spinner /></Page>;
    }

    if (st.get('error')) {
        return (
            <Page title="Produits">
                <Block>
                    <p>Erreur : {st.get('error')}</p>
                    <Button onClick={loadProducts}>Réessayer</Button>
                </Block>
            </Page>
        );
    }

    return (
        <Page title="Produits" onRefresh={loadProducts}>
            <Block>
                <Button onClick={() => nav.navigate('/products/new')}>
                    Nouveau produit
                </Button>
            </Block>

            <Block>
                <List>
                    {st.get('products').map(product => (
                        <ListItem
                            key={product.id}
                            title={product.label}
                            subtitle={`${product.price} €`}
                            onClick={() => nav.navigate(`/products/${product.id}`)}
                            actions={
                                <Button
                                    size="sm"
                                    variant="danger"
                                    onClick={(e) => {
                                        e.stopPropagation();
                                        deleteProduct(product.id);
                                    }}
                                >
                                    Supprimer
                                </Button>
                            }
                        />
                    ))}
                </List>
            </Block>
        </Page>
    );
};

Gestion des erreurs

const api = useApi();

const createProduct = async (data) => {
    try {
        const result = await api.private.post('products', { json: data }).json();
        return { success: true, data: result };
    } catch (error) {
        // Erreur HTTP
        if (error.response) {
            const status = error.response.status;
            const body = await error.response.json().catch(() => ({}));

            if (status === 400) {
                return { success: false, error: 'Données invalides', details: body };
            }
            if (status === 401) {
                return { success: false, error: 'Session expirée' };
            }
            if (status === 403) {
                return { success: false, error: 'Accès interdit' };
            }
            if (status === 404) {
                return { success: false, error: 'Ressource non trouvée' };
            }
            if (status >= 500) {
                return { success: false, error: 'Erreur serveur' };
            }
        }

        // Erreur réseau
        return { success: false, error: 'Erreur de connexion' };
    }
};

Gestion globale des erreurs (onApiError)

Configurez un callback global dans appConfig pour centraliser le traitement des erreurs API :

// src/appConfig.js
import toast from 'react-hot-toast';

export const config = {
    api: {
        prefixUrl: import.meta.env.VITE_API_URL,
        onApiError: (message) => {
            toast.error(message);
        },
    },
};

Quand une requête échoue via les méthodes raccourcies (get, post, put, patch, del), useApi :

  • Extrait le message d'erreur du body JSON (champ error ou message)
  • Appelle onApiError avec ce message (sauf si silent: true)
  • Détecte aussi les erreurs applicatives (HTTP 200 avec champ error dans le body)

Fonctionnalités automatiques

Gestion des tokens JWT

  • Le token d'accès est ajouté automatiquement aux requêtes api.private
  • Le token est rafraîchi automatiquement avant expiration
  • En cas d'erreur 401, un refresh est tenté automatiquement

Headers automatiques

Chaque requête inclut :

  • Authorization: Bearer <accessToken> (requêtes private)
  • X-DEVICEID: <uuid> (identification de l'appareil)
  • Content-Type: application/json

Circuit breaker

Protection contre les requêtes en cascade :

  • Blocage temporaire après plusieurs échecs
  • Détection automatique de la connexion internet

Paramètres de requête

Query string

// GET /products?category=electronics&limit=10
const products = await api.private.get('products', {
    searchParams: {
        category: 'electronics',
        limit: 10
    }
}).json();

Headers personnalisés

const data = await api.private.get('items', {
    headers: {
        'X-Custom-Header': 'value'
    }
}).json();

Timeout personnalisé

const data = await api.private.get('slow-endpoint', {
    timeout: 60000  // 60 secondes
}).json();

Points clés à retenir

  1. api.login() gère automatiquement le stockage des tokens
  2. api.private ajoute automatiquement l'authentification
  3. api.public pour les endpoints sans auth
  4. Terminer par .json() pour parser la réponse
  5. Le refresh token est géré automatiquement
  6. api.get/post/put/patch/del : raccourcis avec gestion d'erreur automatique
  7. api.user : accès direct à l'utilisateur connecté
  8. onApiError : centralise la notification des erreurs
  9. { raw: true } pour les fichiers binaires
  10. { silent: true } pour supprimer les notifications d'erreur

← Retour au module | Chapitre suivant : Gestion d'état ->