SmarMaker - Documentation
Docs» 15_training:module10-fonctionnalites-avancees:offline

Chapitre 1 : Mode offline

Le mode offline permet à l'application de fonctionner sans connexion internet.

Stratégie

  1. Stocker localement les données avec IndexedDB (useDb)
  2. Synchroniser avec le serveur quand la connexion revient
  3. Gérer les conflits si les données ont changé des deux côtés

Configuration de useDb

snippet.javascript
import { useDb } from '@cap-rel/smartcommon';
 
const db = useDb({
    name: 'monApp',
    version: 1,
    stores: {
        // Données métier
        tasks: 'id, ref, label, status, synced, updatedAt',
 
        // File d'attente des modifications
        syncQueue: 'id++, action, entity, entityId, data, createdAt'
    }
});

Hook personnalisé pour le offline

snippet.javascript
// hooks/useOfflineSync.js
import { useState, useEffect } from 'react';
import { useApi, useDb } from '@cap-rel/smartcommon';
 
export function useOfflineSync() {
    const api = useApi();
    const db = useDb({
        name: 'monApp',
        version: 1,
        stores: {
            tasks: 'id, synced',
            syncQueue: 'id++, action, entity, entityId, data'
        }
    });
 
    const [isOnline, setIsOnline] = useState(navigator.onLine);
    const [isSyncing, setIsSyncing] = useState(false);
 
    // Détecter le statut réseau
    useEffect(() => {
        const handleOnline = () => {
            setIsOnline(true);
            sync();  // Synchroniser automatiquement
        };
 
        const handleOffline = () => setIsOnline(false);
 
        window.addEventListener('online', handleOnline);
        window.addEventListener('offline', handleOffline);
 
        return () => {
            window.removeEventListener('online', handleOnline);
            window.removeEventListener('offline', handleOffline);
        };
    }, []);
 
    // Synchroniser les données
    const sync = async () => {
        if (!navigator.onLine || isSyncing) return;
 
        setIsSyncing(true);
 
        try {
            // 1. Envoyer les modifications locales
            const queue = await db.syncQueue.toArray();
 
            for (const item of queue) {
                try {
                    if (item.action === 'create') {
                        await api.private.post(item.entity, { json: item.data });
                    } else if (item.action === 'update') {
                        await api.private.put(`${item.entity}/${item.entityId}`, { json: item.data });
                    } else if (item.action === 'delete') {
                        await api.private.delete(`${item.entity}/${item.entityId}`);
                    }
 
                    // Supprimer de la queue
                    await db.syncQueue.delete(item.id);
                } catch (err) {
                    console.error('Sync error:', err);
                }
            }
 
            // 2. Récupérer les données fraîches du serveur
            const data = await api.private.get('tasks').json();
 
            // 3. Mettre à jour la base locale
            await db.tasks.clear();
            await db.tasks.bulkAdd(data.tasks.map(t => ({ ...t, synced: true })));
 
        } finally {
            setIsSyncing(false);
        }
    };
 
    // Ajouter une action à la queue
    const queueAction = async (action, entity, entityId, data) => {
        await db.syncQueue.add({
            action,
            entity,
            entityId,
            data,
            createdAt: Date.now()
        });
    };
 
    return {
        db,
        isOnline,
        isSyncing,
        sync,
        queueAction
    };
}

Utilisation dans un composant

snippet.javascript
import { useEffect } from 'react';
import { Page, Block, List, ListItem, Spinner, Tag } from '@cap-rel/smartcommon';
import { useApi, useStates } from '@cap-rel/smartcommon';
import { useOfflineSync } from '../../hooks/useOfflineSync';
 
export const TasksPage = () => {
    const api = useApi();
    const { db, isOnline, isSyncing, sync, queueAction } = useOfflineSync();
 
    const st = useStates({
        initialStates: {
            tasks: [],
            loading: true
        }
    });
 
    useEffect(() => {
        loadTasks();
    }, []);
 
    const loadTasks = async () => {
        st.set('loading', true);
 
        try {
            if (isOnline) {
                // En ligne : charger depuis l'API
                const data = await api.private.get('tasks').json();
 
                // Sauvegarder localement
                await db.tasks.clear();
                await db.tasks.bulkAdd(data.tasks.map(t => ({ ...t, synced: true })));
 
                st.set('tasks', data.tasks);
            } else {
                // Hors ligne : charger depuis IndexedDB
                const localTasks = await db.tasks.toArray();
                st.set('tasks', localTasks);
            }
        } catch (err) {
            // Erreur réseau : utiliser les données locales
            const localTasks = await db.tasks.toArray();
            st.set('tasks', localTasks);
        } finally {
            st.set('loading', false);
        }
    };
 
    const createTask = async (data) => {
        // Créer localement avec un ID temporaire
        const tempId = 'temp_' + Date.now();
        const newTask = { ...data, id: tempId, synced: false };
 
        await db.tasks.add(newTask);
        st.set('tasks', [...st.get('tasks'), newTask]);
 
        if (isOnline) {
            try {
                const result = await api.private.post('tasks', { json: data }).json();
                // Remplacer l'ID temporaire par le vrai ID
                await db.tasks.update(tempId, { id: result.id, synced: true });
            } catch (err) {
                // Ajouter à la queue pour sync ultérieure
                await queueAction('create', 'tasks', tempId, data);
            }
        } else {
            await queueAction('create', 'tasks', tempId, data);
        }
    };
 
    return (
        <Page title="Tâches">
            {/* Indicateur de statut */}
            <Block>
                <div className="flex items-center gap-2">
                    <Tag color={isOnline ? 'green' : 'red'}>
                        {isOnline ? 'En ligne' : 'Hors ligne'}
                    </Tag>
                    {isSyncing && <Spinner size="sm" />}
                </div>
            </Block>
 
            {/* Liste */}
            <Block>
                <List>
                    {st.get('tasks').map(task => (
                        <ListItem
                            key={task.id}
                            title={task.label}
                            subtitle={!task.synced ? '⏳ En attente de sync' : null}
                        />
                    ))}
                </List>
            </Block>
        </Page>
    );
};

Gestion des conflits

snippet.javascript
const resolveConflict = async (localData, serverData) => {
    // Stratégie simple : le plus récent gagne
    if (localData.updatedAt > serverData.updatedAt) {
        // Envoyer les données locales au serveur
        await api.private.put(`tasks/${localData.id}`, { json: localData });
    } else {
        // Remplacer localement par les données serveur
        await db.tasks.put(serverData);
    }
};

Points clés à retenir

  1. useDb pour le stockage IndexedDB
  2. Détecter le statut réseau avec navigator.onLine
  3. Queue de synchronisation pour les actions offline
  4. Synchroniser quand la connexion revient
  5. Gérer les conflits selon une stratégie définie

← Retour au module | Chapitre suivant : Internationalisation →

Previous Next

Made with ❤ by CAP-REL · SmartMaker · GNU AGPL v3+
Code source · Faire un don
SmarMaker - Documentation
Traductions de cette page:
  • Français
  • Deutsch
  • English
  • Español
  • Italiano
  • Nederlands

Table of Contents

Table des matières

  • Chapitre 1 : Mode offline
    • Stratégie
    • Configuration de useDb
    • Hook personnalisé pour le offline
    • Utilisation dans un composant
    • Gestion des conflits
    • Points clés à retenir
  • SmartAuth
  • SmartMaker - Back (PHP)
    • Mapping Dolibarr - React
  • SmartMaker - Front (React)
    • Animations de pages
    • Architecture
    • Astuces
    • Calendar
    • Composants et pages
    • Configuration du Provider
    • Debug et Logs
    • Hooks SmartCommon
    • PWA (Progressive Web App)
    • Requêtes API
    • Routage
    • SmartCommon
    • Stockage de données
    • Thèmes
    • Traductions
  • HowTo - Pas à pas - Votre première application
    • Développement PHP (back)
    • Développement React (front)
    • Première étape : Module Builder Dolibarr
    • SmartAuth
    • SmartBoot : Un "squelette" quasiment prêt à l'emploi
  • Formation SmartMaker
    • Module 1 : Fondamentaux JavaScript ES6+
      • Chapitre 1 : Variables et Scope
      • Chapitre 2 : Fonctions
      • Chapitre 3 : Programmation Asynchrone
      • Chapitre 4 : Modules ES6
    • Module 2 : Introduction à React
      • Chapitre 1 : Philosophie React
      • Chapitre 2 : JSX
      • Chapitre 3 : Composants
    • Module 3 : Hooks React Fondamentaux
      • Chapitre 1 : useState
      • Chapitre 2 : useEffect
      • Chapitre 3 : useRef
      • Chapitre 4 : useContext
    • Module 4 : React Avancé
      • Chapitre 1 : useCallback et useMemo
      • Chapitre 2 : Custom Hooks
      • Chapitre 3 : Redux et Redux Toolkit
    • Module 5 : Architecture SmartMaker
      • Chapitre 1 : Structure du projet
      • Chapitre 2 : Configuration
      • Chapitre 3 : Flux de données
    • Module 6 : SmartCommon - Composants
      • Chapitre 1 : Mise en page
      • Chapitre 2 : Navigation
      • Chapitre 3 : Formulaires
      • Chapitre 4 : Affichage
    • Module 7 : SmartCommon - Hooks
      • Chapitre 1 : useApi
      • Chapitre 2 : Gestion d'état
      • Chapitre 3 : Hooks utilitaires
    • Module 8 : Backend API (PHP)
      • Chapitre 1 : Routage
      • Chapitre 2 : Controllers
      • Chapitre 3 : Mappers
      • Extrafields et formulaires dynamiques
    • Module 9 : Intégration complète
      • Chapitre 1 : Backend
      • Chapitre 2 : Frontend
      • Chapitre 3 : Déploiement
    • Module 10 : Fonctionnalités avancées
      • Chapitre 1 : Mode offline
      • Chapitre 2 : Internationalisation (i18n)
      • Chapitre 3 : Autres fonctionnalités
    • Module 11 : Bonnes pratiques
  • Démonstration
  • Start
  • Composants et pages
  • Afficher le texte source
  • Anciennes révisions
  • Liens de retour
  • Haut de page