Skip to main content

Overview

MotorDesk implements a robust offline-first architecture that allows users to continue working even without internet connectivity. All data is persisted locally and synchronized automatically when the connection is restored.

Storage Architecture

LocalForage Configuration

The application uses LocalForage as an abstraction layer over IndexedDB for reliable local storage:
// src/store/index.ts:10-13
localforage.config({
  name: 'FleetSUNAT_DB',
  storeName: 'redux_state'
});
LocalForage automatically falls back to WebSQL or localStorage if IndexedDB is not available, ensuring compatibility across browsers.

Redux Persist Configuration

Redux Persist is configured to automatically save critical application state to IndexedDB:
// src/store/index.ts:15-19
const persistConfig = {
  key: 'sunat-root',
  storage: localforage,
  whitelist: ['auth', 'sales'],
};

Persisted State

Only auth and sales slices are persisted. This selective approach keeps storage lean while ensuring critical data survives page refreshes and offline periods.

Persistence Flow

Sync Status Management

Sale Sync States

Each sale in the system has a sync_status field that tracks its synchronization state:
// src/store/slices/salesSlice.ts:3-8
interface Sale {
    id: string;
    clienteId: string;
    montoTotal: number;
    sync_status: 'PENDING' | 'SYNCED';
}
Sale was created offline or failed to sync. Will be synchronized on next reconnection.
Sale has been successfully synchronized with the backend server.

Adding Sales Offline

When a sale is created, it’s immediately added to local state with PENDING status:
// src/store/slices/salesSlice.ts:22-24
addSaleRequest: (state, action: PayloadAction<Omit<Sale, 'sync_status'>>) => {
    state.items.push({ ...action.payload, sync_status: 'PENDING' });
}
1

Immediate Local Storage

Sale is added to Redux state and immediately persisted to IndexedDB
2

User Confirmation

UI updates instantly, providing immediate feedback to the user
3

Background Sync

If online, saga attempts to sync in the background
4

Status Update

Once synced, status changes from PENDING to SYNCED

Synchronization Logic

Redux Saga Implementation

The rootSaga.ts file orchestrates all synchronization logic using Redux Saga:
// src/store/rootSaga.ts:8-20
function* syncPendingSales() {
  if (!navigator.onLine) return;
  const pendingSales: ReturnType<typeof getPendingSales> = yield select(getPendingSales);

  for (const sale of pendingSales) {
    try {
      yield delay(1000);
      yield put(updateSaleStatus({ id: sale.id, status: 'SYNCED' }));
    } catch (error) {
      console.error('Error sincronizando venta:', sale.id);
    }
  }
}
The current implementation includes a 1-second delay between sync attempts to avoid overwhelming the server. Adjust this value based on your backend capacity.

Network Detection

The sync process checks network status before attempting synchronization:
// src/store/rootSaga.ts:22-27
function* handleAddSale(action: any) {
  if (navigator.onLine) {
    yield delay(500);
    yield put(addSaleSuccess(action.payload.id));
  }
}

Online Detection

Uses the navigator.onLine property to detect network connectivity. Note that this only detects if the device has a network connection, not if the backend is reachable.

Reconnection Handling

Sync Triggers

The saga listens for two primary triggers to initiate synchronization:
// src/store/rootSaga.ts:29-34
export default function* rootSaga() {
  yield all([
    takeEvery(addSaleRequest.type, handleAddSale),
    takeEvery('app/RECONNECTED', syncPendingSales)
  ]);
}

On Sale Creation

When a sale is created, attempt immediate sync if online

On Reconnection

When the app detects reconnection, sync all pending sales

Implementing Reconnection Detection

To dispatch the app/RECONNECTED action, add network listeners in your app:
// Example implementation (not in codebase)
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';

function useNetworkStatus() {
  const dispatch = useDispatch();

  useEffect(() => {
    const handleOnline = () => {
      dispatch({ type: 'app/RECONNECTED' });
    };

    window.addEventListener('online', handleOnline);
    return () => window.removeEventListener('online', handleOnline);
  }, [dispatch]);
}

Middleware Configuration

Redux Store Setup

The store is configured with custom middleware to support both Redux Saga and Redux Persist:
// src/store/index.ts:29-38
export const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      thunk: false,
      serializableCheck: {
        ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE', 'persist/REGISTER'],
      },
    }).concat(sagaMiddleware),
});
Thunk middleware is disabled in favor of Redux Saga. Redux Persist actions are excluded from serialization checks to prevent console warnings.

Saga Middleware Initialization

// src/store/index.ts:27,40
const sagaMiddleware = createSagaMiddleware();
// ... store configuration ...
sagaMiddleware.run(rootSaga);

Sync Flow Diagram

State Rehydration

On Application Load

When the application starts, Redux Persist automatically rehydrates state from IndexedDB:
// src/store/index.ts:42
export const persistor = persistStore(store);
The persistor should be passed to PersistGate in your app root:
// Example root setup
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from './store';

<Provider store={store}>
  <PersistGate loading={null} persistor={persistor}>
    <App />
  </PersistGate>
</Provider>

Loading State

The loading prop can be set to a loading component that displays while state is being rehydrated from storage.

Best Practices

Only persist essential state slices. In MotorDesk, only auth and sales are persisted to minimize storage usage and improve performance.
Always update the UI immediately when users take actions, even if the backend sync happens later. This provides better user experience.
Show users which items are pending synchronization with visual indicators (badges, icons) in the UI.
Implement proper error handling in sagas to prevent sync failures from crashing the app. Log errors for debugging.
Implement conflict resolution strategies for cases where local data conflicts with server data during sync.

Storage Capacity

IndexedDB storage limits vary by browser:
  • Chrome/Edge: ~60% of available disk space per origin
  • Firefox: ~50% of available disk space per origin
  • Safari: ~1GB per origin
Monitor storage usage and implement cleanup strategies for older data.

Testing Offline Functionality

To test offline functionality during development:
1

Open DevTools

Open Chrome DevTools or your browser’s developer tools
2

Navigate to Network Tab

Go to the Network tab
3

Set Offline Mode

Change the throttling dropdown to “Offline”
4

Test Operations

Create sales, navigate pages, and verify data persists
5

Go Back Online

Set throttling back to “No throttling”
6

Verify Sync

Check that pending operations sync automatically

Next Steps

State Management

Learn more about Redux slices and saga patterns

Architecture

Understand the overall system architecture

Build docs developers (and LLMs) love