---
source_hash: "ed1ce650"
title: "Chapter 1: Offline Mode"
weight: 710
---

# Chapter 1: Offline Mode

Offline mode lets the application work without an internet connection.

## Strategy

1. **Store locally** using IndexedDB (useDb)
2. **Synchronize** with the server when the connection comes back
3. **Handle conflicts** when the data changed on both sides

## useDb configuration

```javascript
import { useDb } from '@cap-rel/smartcommon';

const db = useDb({
    name: 'monApp',
    version: 1,
    stores: {
        // Business data
        tasks: 'id, ref, label, status, synced, updatedAt',

        // Pending changes queue
        syncQueue: 'id++, action, entity, entityId, data, createdAt'
    }
});
```

## Custom hook for 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);

    // Detect network status
    useEffect(() => {
        const handleOnline = () => {
            setIsOnline(true);
            sync();  // Synchronize automatically
        };

        const handleOffline = () => setIsOnline(false);

        window.addEventListener('online', handleOnline);
        window.addEventListener('offline', handleOffline);

        return () => {
            window.removeEventListener('online', handleOnline);
            window.removeEventListener('offline', handleOffline);
        };
    }, []);

    // Synchronize the data
    const sync = async () => {
        if (!navigator.onLine || isSyncing) return;

        setIsSyncing(true);

        try {
            // 1. Send the local changes
            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}`);
                    }

                    // Remove from the queue
                    await db.syncQueue.delete(item.id);
                } catch (err) {
                    console.error('Sync error:', err);
                }
            }

            // 2. Fetch fresh data from the server
            const data = await api.private.get('tasks').json();

            // 3. Update the local database
            await db.tasks.clear();
            await db.tasks.bulkAdd(data.tasks.map(t => ({ ...t, synced: true })));

        } finally {
            setIsSyncing(false);
        }
    };

    // Add an action to the 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
    };
}
```

## Usage in a component

```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) {
                // Online: load from the API
                const data = await api.private.get('tasks').json();

                // Save locally
                await db.tasks.clear();
                await db.tasks.bulkAdd(data.tasks.map(t => ({ ...t, synced: true })));

                st.set('tasks', data.tasks);
            } else {
                // Offline: load from IndexedDB
                const localTasks = await db.tasks.toArray();
                st.set('tasks', localTasks);
            }
        } catch (err) {
            // Network error: fall back on the local data
            const localTasks = await db.tasks.toArray();
            st.set('tasks', localTasks);
        } finally {
            st.set('loading', false);
        }
    };

    const createTask = async (data) => {
        // Create locally with a temporary ID
        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();
                // Replace the temporary ID with the real one
                await db.tasks.update(tempId, { id: result.id, synced: true });
            } catch (err) {
                // Add to the queue for a later sync
                await queueAction('create', 'tasks', tempId, data);
            }
        } else {
            await queueAction('create', 'tasks', tempId, data);
        }
    };

    return (
        <Page title="Tasks">
            {/* Status indicator */}
            <Block>
                <div className="flex items-center gap-2">
                    <Tag color={isOnline ? 'green' : 'red'}>
                        {isOnline ? 'Online' : 'Offline'}
                    </Tag>
                    {isSyncing && <Spinner size="sm" />}
                </div>
            </Block>

            {/* List */}
            <Block>
                <List>
                    {st.get('tasks').map(task => (
                        <ListItem
                            key={task.id}
                            title={task.label}
                            subtitle={!task.synced ? '⏳ Waiting for sync' : null}
                        />
                    ))}
                </List>
            </Block>
        </Page>
    );
};
```

## Conflict handling

```javascript
const resolveConflict = async (localData, serverData) => {
    // Simple strategy: the most recent one wins
    if (localData.updatedAt > serverData.updatedAt) {
        // Send the local data to the server
        await api.private.put(`tasks/${localData.id}`, { json: localData });
    } else {
        // Replace locally with the server data
        await db.tasks.put(serverData);
    }
};
```

## Key takeaways

1. **useDb** for IndexedDB storage
2. **Detect the network status** with navigator.onLine
3. **Synchronization queue** for offline actions
4. **Synchronize** when the connection comes back
5. **Handle conflicts** according to a defined strategy

[Back to Module](/training/module10-fonctionnalites-avancees) | [Next Chapter: Internationalization ->](/training/module10-fonctionnalites-avancees/i18n)
