---
title: "Chapitre 1 : Mode offline"
weight: 710
---

# 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

```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

```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.del(`${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

```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

```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](/training/module10-fonctionnalites-avancees) | [Chapitre suivant : Internationalisation ->](/training/module10-fonctionnalites-avancees/i18n)
