Chapitre 1 : Mode offline
Le mode offline permet à l'application de fonctionner sans connexion internet.
Stratégie
- Stocker localement les données avec IndexedDB (useDb)
- Synchroniser avec le serveur quand la connexion revient
- Gérer les conflits si les données ont changé des deux côtés
Configuration de useDb
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
// 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
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
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
- useDb pour le stockage IndexedDB
- Détecter le statut réseau avec navigator.onLine
- Queue de synchronisation pour les actions offline
- Synchroniser quand la connexion revient
- Gérer les conflits selon une stratégie définie
← Retour au module | Chapitre suivant : Internationalisation ->