SmarMaker - Documentation
Docs» 15_training:module7-smartcommon-hooks:sync

Chapitre 4 : Synchronisation Offline

SmartCommon fournit un module complet pour la synchronisation offline-first des applications PWA Dolibarr.

Vue d'ensemble

Le module sync permet de :

  • Travailler hors connexion avec les données locales
  • Synchroniser automatiquement quand la connexion revient
  • Gérer les conflits de données entre client et serveur

useSyncClient

Hook principal pour la synchronisation offline-first.

Import

snippet.javascript
import { useSyncClient } from '@cap-rel/smartcommon';

Configuration

snippet.javascript
function MyApp() {
    const {
        isOnline,
        isSyncing,
        pendingCount,
        sync,
        create,
        update,
        remove,
        getConflicts,
        resolveConflict
    } = useSyncClient({
        apiUrl: '/api/smartauth',
        getAccessToken: () => localStorage.getItem('access_token'),
        scope: ['thirdparty', 'contact', 'product']
    });
 
    return (
        <div>
            <p>Status : {isOnline ? 'En ligne' : 'Hors ligne'}</p>
            <p>Modifications en attente : {pendingCount}</p>
        </div>
    );
}

Paramètres

Paramètre Type Description
apiUrl string URL de base de l'API de synchronisation
getAccessToken function Fonction retournant le token JWT
scope string[] Liste des entités à synchroniser

Valeurs retournées

Propriété Type Description
isOnline boolean Statut de connexion
isSyncing boolean Synchronisation en cours
pendingCount number Nombre de modifications en attente
sync function Déclencher la synchronisation
create function Créer une entité (offline-capable)
update function Modifier une entité
remove function Supprimer une entité
upsert function Créer ou mettre à jour localement (cache)
getConflicts function Récupérer les conflits
resolveConflict function Résoudre un conflit

Créer une entité

snippet.javascript
function CreateThirdpartyForm() {
    const { create, pendingCount } = useSyncClient({
        apiUrl: '/api/smartauth',
        getAccessToken: () => localStorage.getItem('access_token'),
        scope: ['thirdparty']
    });
 
    const handleCreate = async (data) => {
        // Crée localement avec un ID temporaire
        // Sera synchronisé quand la connexion revient
        const tempId = await create('thirdparty', {
            name: data.name,
            email: data.email,
            phone: data.phone
        });
 
        console.log('Créé avec ID temporaire:', tempId);
    };
 
    return (
        <form onSubmit={handleSubmit}>
            {/* ... */}
            <p>En attente de sync : {pendingCount}</p>
        </form>
    );
}

Modifier et supprimer

snippet.javascript
function ThirdpartyActions({ thirdparty }) {
    const { update, remove } = useSyncClient({
        apiUrl: '/api/smartauth',
        getAccessToken: () => localStorage.getItem('access_token'),
        scope: ['thirdparty']
    });
 
    const handleUpdate = async () => {
        await update('thirdparty', thirdparty.id, {
            name: 'Nouveau nom'
        });
    };
 
    const handleDelete = async () => {
        await remove('thirdparty', thirdparty.id);
    };
 
    return (
        <div>
            <button onClick={handleUpdate}>Modifier</button>
            <button onClick={handleDelete}>Supprimer</button>
        </div>
    );
}

Upsert (cache local)

La méthode upsert permet de stocker des données localement sans déclencher de synchronisation vers le serveur. Elle crée l'entité si elle n'existe pas, ou la met à jour si elle existe déjà.

snippet.javascript
function ThirdpartyDetail({ id }) {
    const { upsert, getEntity } = useSyncClient({
        apiUrl: '/api/smartauth',
        getAccessToken: () => localStorage.getItem('access_token'),
        scope: ['thirdparty']
    });
 
    const cacheServerData = async () => {
        // Récupérer les données du serveur
        const data = await api.private.get(`thirdparties/${id}`).json();
 
        // Stocker localement sans déclencher de sync
        await upsert('thirdparty', id, data);
    };
 
    // Avec queueChange = true, la modification sera synchronisée
    const upsertAndSync = async (data) => {
        await upsert('thirdparty', id, data, true);
    };
 
    // ...
}

Paramètres

Paramètre Type Défaut Description
table string - Nom de la table
id number/string - ID de l'entité
data object - Données de l'entité
queueChange boolean false Si true, ajoute la modification à la queue de sync

Synchronisation manuelle

snippet.javascript
function SyncButton() {
    const { sync, isSyncing, pendingCount, isOnline } = useSyncClient({
        apiUrl: '/api/smartauth',
        getAccessToken: () => localStorage.getItem('access_token'),
        scope: ['thirdparty', 'contact']
    });
 
    const handleSync = async () => {
        const result = await sync();
        console.log('Synchronisé:', result);
    };
 
    return (
        <button
            onClick={handleSync}
            disabled={isSyncing || !isOnline || pendingCount === 0}
        >
            {isSyncing ? 'Synchronisation...' : `Synchroniser (${pendingCount})`}
        </button>
    );
}

ConflictResolver

Composant UI pour résoudre les conflits de synchronisation.

Import

snippet.javascript
import { ConflictResolver } from '@cap-rel/smartcommon';

Utilisation

snippet.javascript
function SyncManager() {
    const {
        getConflicts,
        resolveConflict
    } = useSyncClient({
        apiUrl: '/api/smartauth',
        getAccessToken: () => localStorage.getItem('access_token'),
        scope: ['thirdparty']
    });
 
    const [conflicts, setConflicts] = useState([]);
 
    useEffect(() => {
        loadConflicts();
    }, []);
 
    const loadConflicts = async () => {
        const list = await getConflicts();
        setConflicts(list);
    };
 
    const handleResolve = async (conflictId, resolution) => {
        await resolveConflict(conflictId, resolution);
        await loadConflicts();
    };
 
    if (conflicts.length === 0) {
        return <p>Aucun conflit</p>;
    }
 
    return (
        <ConflictResolver
            conflicts={conflicts}
            onResolve={handleResolve}
        />
    );
}

Props

Prop Type Description
conflicts array Liste des conflits à afficher
onResolve function Callback appelé lors de la résolution

Structure d'un conflit

snippet.javascript
{
    id: 'conflict_123',
    entity: 'thirdparty',
    entityId: 456,
    localData: { name: 'Version locale', ... },
    serverData: { name: 'Version serveur', ... },
    localTimestamp: 1707900000000,
    serverTimestamp: 1707899000000
}

Résolutions possibles

  • 'local' : Garder la version locale
  • 'server' : Garder la version serveur
  • 'merge' : Fusionner (si supporté)

useOnlineStatus

Hook pour détecter le statut online/offline avec health check serveur optionnel.

Import

snippet.javascript
import { useOnlineStatus } from '@cap-rel/smartcommon';

Utilisation simple

snippet.javascript
function NetworkStatus() {
    const { isOnline, isOffline } = useOnlineStatus();
 
    return (
        <div className={isOffline ? 'bg-red-500' : 'bg-green-500'}>
            {isOnline ? 'En ligne' : 'Hors ligne'}
        </div>
    );
}

Avec health check serveur

snippet.javascript
function ServerStatus() {
    const {
        isOnline,
        isServerReachable,
        lastCheck,
        checkNow
    } = useOnlineStatus({
        healthCheckUrl: '/api/health',
        healthCheckInterval: 60000,  // Vérifier toutes les 60s
        stabilityDelay: 2000,        // Attendre 2s avant de déclarer "en ligne"
        timeout: 5000                // Timeout de 5s
    });
 
    return (
        <div>
            <p>Navigateur : {isOnline ? 'En ligne' : 'Hors ligne'}</p>
            <p>Serveur : {isServerReachable ? 'Accessible' : 'Inaccessible'}</p>
            <p>Dernière vérification : {new Date(lastCheck).toLocaleTimeString()}</p>
            <button onClick={checkNow}>Vérifier maintenant</button>
        </div>
    );
}

Paramètres

Paramètre Type Défaut Description
healthCheckUrl string null URL pour vérifier le serveur (null = désactivé)
healthCheckInterval number 30000 Intervalle entre les vérifications (ms)
stabilityDelay number 2000 Délai avant de déclarer “en ligne” (ms)
timeout number 5000 Timeout du health check (ms)

Valeurs retournées

Propriété Type Description
isOnline boolean Navigateur en ligne
isOffline boolean Navigateur hors ligne
isServerReachable boolean/null Serveur accessible (null si non testé)
lastOnline number Timestamp du dernier état “en ligne”
lastCheck number Timestamp de la dernière vérification
checkNow function Forcer une vérification immédiate

useCachedQuery

Hook pour le cache de requêtes avec stratégies multiples.

Import

snippet.javascript
import { useCachedQuery, CACHE_STRATEGIES } from '@cap-rel/smartcommon';

Stratégies disponibles

Stratégie Description
NETWORKFIRST | Réseau d'abord, cache en fallback | | CACHEFIRST Cache d'abord si valide, sinon réseau
STALEWHILEREVALIDATE Afficher le cache, rafraîchir en arrière-plan

Exemple : Cache-first pour dictionnaires

snippet.javascript
function CountrySelect() {
    const { data: countries, isLoading, isFromCache } = useCachedQuery({
        db: db.instance,
        store: 'queryCache',
        key: 'countries',
        fetchFn: () => api.get('dictionaries/countries').json(),
        strategy: CACHE_STRATEGIES.CACHE_FIRST,
        ttl: 86400000  // 24h
    });
 
    if (isLoading) return <Spinner />;
 
    return (
        <select>
            {countries.map(c => (
                <option key={c.code} value={c.code}>{c.label}</option>
            ))}
        </select>
    );
}

Exemple : Stale-while-revalidate pour config

snippet.javascript
function AppConfig() {
    const {
        data: config,
        isStale,
        refetch,
        invalidate
    } = useCachedQuery({
        db: db.instance,
        store: 'queryCache',
        key: 'app-config',
        fetchFn: () => api.get('config').json(),
        strategy: CACHE_STRATEGIES.STALE_WHILE_REVALIDATE,
        staleTime: 300000  // 5 min
    });
 
    return (
        <div>
            {isStale && <p>Mise à jour en cours...</p>}
            <button onClick={invalidate}>Forcer le rafraîchissement</button>
        </div>
    );
}

Paramètres

Paramètre Type Défaut Description
db object - Instance Dexie (db.instance)
store string - Nom du store IndexedDB
key string - Clé de cache
fetchFn function - Fonction de récupération des données
strategy string NETWORK_FIRST Stratégie de cache
ttl number 3600000 Durée de vie du cache (1h)
staleTime number 60000 Temps avant données “stale” (1min)
enabled boolean true Activer/désactiver le fetch

Valeurs retournées

Propriété Type Description
data any Données récupérées/cachées
isLoading boolean Chargement en cours
isFromCache boolean Données provenant du cache
isStale boolean Données périmées
error Error Erreur éventuelle
lastFetch number Timestamp du dernier fetch
refetch function Relancer le fetch
invalidate function Vider le cache et refetch

useAuthenticatedImage

Hook pour charger des images authentifiées avec cache IndexedDB.

Import

snippet.javascript
import { useAuthenticatedImage } from '@cap-rel/smartcommon';

Utilisation

snippet.javascript
function UserAvatar({ userId }) {
    const { src, isLoading, isFromCache, error } = useAuthenticatedImage({
        db: db.instance,
        store: 'imageCache',
        url: `/api/users/${userId}/photo`,
        token: accessToken,
        placeholder: '/images/default-avatar.png',
        ttl: 86400000,    // 24h
        staleTime: 3600000 // 1h
    });
 
    if (isLoading) return <Spinner />;
 
    return <img src={src} alt="Avatar" />;
}

Paramètres

Paramètre Type Défaut Description
db object - Instance Dexie
store string 'imageCache' Nom du store
url string - URL de l'image
token string - Token JWT
ttl number 86400000 Durée de vie (24h)
staleTime number 3600000 Temps avant stale (1h)
placeholder string null Image par défaut

Valeurs retournées

Propriété Type Description
src string URL de l'image (blob ou placeholder)
isLoading boolean Chargement en cours
isFromCache boolean Image provenant du cache
error Error Erreur éventuelle

Configuration IndexedDB

Pour utiliser useCachedQuery et useAuthenticatedImage, configurer les stores Dexie :

snippet.javascript
const db = useDb({
    name: 'myApp',
    version: 2,
    stores: {
        // Store pour les requêtes cachées
        queryCache: 'key',
 
        // Store pour les images
        imageCache: 'key',
 
        // Autres stores...
        items: 'id++, name'
    }
});

Exemple complet : Application offline-first

snippet.javascript
import { useEffect } from 'react';
import {
    useSyncClient,
    useOnlineStatus,
    useCachedQuery,
    useDb,
    Page,
    Block,
    List,
    ListItem,
    Button,
    ConflictResolver
} from '@cap-rel/smartcommon';
 
function ThirdpartyList() {
    const db = useDb({
        name: 'myApp',
        version: 1,
        stores: {
            queryCache: 'key',
            pendingChanges: 'id++, entity, action'
        }
    });
 
    const { isOnline } = useOnlineStatus({
        healthCheckUrl: '/api/health'
    });
 
    const {
        sync,
        isSyncing,
        pendingCount,
        getConflicts
    } = useSyncClient({
        apiUrl: '/api/smartauth',
        getAccessToken: () => localStorage.getItem('access_token'),
        scope: ['thirdparty']
    });
 
    const {
        data: thirdparties,
        isLoading,
        isFromCache,
        refetch
    } = useCachedQuery({
        db: db.instance,
        store: 'queryCache',
        key: 'thirdparties',
        fetchFn: () => api.get('thirdparties').json(),
        strategy: 'swr'
    });
 
    // Synchroniser automatiquement quand on revient en ligne
    useEffect(() => {
        if (isOnline && pendingCount > 0) {
            sync();
        }
    }, [isOnline]);
 
    return (
        <Page title="Tiers">
            <Block>
                <div className="flex justify-between items-center">
                    <span>
                        {isOnline ? 'En ligne' : 'Hors ligne'}
                        {isFromCache && ' (cache)'}
                    </span>
                    {pendingCount > 0 && (
                        <Button
                            onClick={sync}
                            disabled={!isOnline || isSyncing}
                        >
                            Sync ({pendingCount})
                        </Button>
                    )}
                </div>
            </Block>
 
            <Block>
                <List>
                    {thirdparties?.map(t => (
                        <ListItem key={t.id}>
                            {t.name}
                        </ListItem>
                    ))}
                </List>
            </Block>
        </Page>
    );
}

Points clés à retenir

  1. useSyncClient pour les opérations CRUD offline-capable
  2. useOnlineStatus pour détecter la connectivité
  3. useCachedQuery pour le cache intelligent avec stratégies
  4. useAuthenticatedImage pour les images protégées
  5. ConflictResolver pour la résolution de conflits UI
  6. Configurer les stores IndexedDB pour le cache

← Chapitre précédent | Retour au module

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 4 : Synchronisation Offline
    • Vue d'ensemble
    • useSyncClient
      • Import
      • Configuration
      • Paramètres
      • Valeurs retournées
      • Créer une entité
      • Modifier et supprimer
      • Upsert (cache local)
      • Synchronisation manuelle
    • ConflictResolver
      • Import
      • Utilisation
      • Props
      • Structure d'un conflit
      • Résolutions possibles
    • useOnlineStatus
      • Import
      • Utilisation simple
      • Avec health check serveur
      • Paramètres
      • Valeurs retournées
    • useCachedQuery
      • Import
      • Stratégies disponibles
      • Exemple : Cache-first pour dictionnaires
      • Exemple : Stale-while-revalidate pour config
      • Paramètres
      • Valeurs retournées
    • useAuthenticatedImage
      • Import
      • Utilisation
      • Paramètres
      • Valeurs retournées
    • Configuration IndexedDB
    • Exemple complet : Application offline-first
    • 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
    • Synchronisation offline
    • 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 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
      • Chapitre 4 : Synchronisation Offline
    • 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