# Offline-First Redux Architecture This directory contains the offline-first Redux store implementation for the Maternal App. ## Architecture Overview The store is built with: - **Redux Toolkit** - Modern Redux with less boilerplate - **@redux-offline/redux-offline** - Offline-first middleware - **redux-persist** - State persistence to IndexedDB - **Entity Adapters** - Normalized state management ## Key Features ### 1. Normalized State All entities (activities, children) are stored in a normalized format with `byId` and `allIds` structures for optimal performance and consistency. ### 2. Optimistic Updates Actions are immediately applied to the UI, then synced with the server: ```typescript // Component usage const dispatch = useAppDispatch(); // Optimistic create - UI updates immediately dispatch(optimisticCreate(newActivity)); // Actual API call - syncs with server dispatch(createActivity(newActivity)); ``` ### 3. Offline Queue When offline, mutations are queued and automatically synced when connection is restored: - Automatic exponential backoff retry (1s, 2s, 4s, 8s, 16s) - Discard after 5 retries or on 4xx errors - Persisted to IndexedDB ### 4. Conflict Resolution Multiple strategies available: - `SERVER_WINS` - Server data takes precedence - `CLIENT_WINS` - Local data takes precedence - `LAST_WRITE_WINS` - Compare timestamps (default) - `MERGE` - Merge local and server data ### 5. Network Detection - Browser online/offline events - Periodic server connectivity checks (every 30s) - Latency-based connection quality (`excellent`, `good`, `poor`, `offline`) ## Directory Structure ``` store/ ├── store.ts # Main store configuration ├── hooks.ts # Custom Redux hooks ├── slices/ # Redux slices │ ├── activitiesSlice.ts # Activities with normalized state │ ├── childrenSlice.ts # Children with normalized state │ ├── networkSlice.ts # Network status │ └── offlineSlice.ts # Offline sync queue └── middleware/ # Custom middleware ├── offlineMiddleware.ts # Offline action queuing └── syncMiddleware.ts # Sync and conflict resolution ``` ## Usage Examples ### Basic Component Usage ```typescript import { useAppDispatch, useAppSelector, useIsOnline } from '@/store/hooks'; import { createActivity, optimisticCreate } from '@/store/slices/activitiesSlice'; function TrackingComponent() { const dispatch = useAppDispatch(); const isOnline = useIsOnline(); const activities = useAppSelector(activitiesSelectors.selectAll); const handleAddActivity = async (activity) => { // 1. Optimistic update (immediate UI feedback) const localId = `temp_${Date.now()}`; dispatch(optimisticCreate({ ...activity, id: localId })); // 2. Actual API call (will be queued if offline) try { await dispatch(createActivity(activity)); } catch (error) { // Rollback handled automatically console.error('Failed to create activity:', error); } }; return (
{!isOnline &&

You are offline. Changes will sync when you reconnect.

} {/* ... */}
); } ``` ### Using Custom Hooks ```typescript import { useOptimisticAction, useSyncStatus, useHasPendingSync } from '@/store/hooks'; function MyComponent() { // Check sync status const { syncing, pendingCount, lastSync } = useSyncStatus(); const hasPending = useHasPendingSync(); // Optimistic action hook (combines optimistic + actual) const handleCreate = useOptimisticAction( optimisticCreate, createActivity ); return (
{syncing &&

Syncing {pendingCount} changes...

}
); } ``` ### Network Status Indicator ```typescript import { NetworkStatusIndicator, NetworkStatusBadge } from '@/components/common/NetworkStatusIndicator'; function AppLayout() { return ( <> {/* Snackbar at top */} {/* Small badge in app bar */} ); } ``` ### Checking if Entity is Optimistic ```typescript import { useIsOptimistic } from '@/store/hooks'; function ActivityCard({ activityId }) { const isOptimistic = useIsOptimistic('activities', activityId); return ( {isOptimistic && ( )} {/* ... */} ); } ``` ## State Shape ```typescript { activities: { ids: ['act_1', 'act_2', 'temp_123'], entities: { 'act_1': { id: 'act_1', childId: 'child_1', type: 'feeding', timestamp: '2025-10-01T12:00:00Z', data: { amount: 120, unit: 'ml' }, _optimistic: false, _version: 1 }, 'temp_123': { id: 'temp_123', childId: 'child_1', type: 'sleep', timestamp: '2025-10-01T14:00:00Z', data: { duration: 90 }, _optimistic: true, // Not yet synced _localId: 'temp_123', _version: 1 } }, loading: false, error: null, syncStatus: 'synced', lastSyncTime: '2025-10-01T14:05:00Z' }, children: { /* similar structure */ }, network: { isOnline: true, isConnected: true, lastOnlineTime: '2025-10-01T14:00:00Z', connectionQuality: 'excellent', latency: 45 }, offline: { isOnline: true, pendingActions: [], lastSyncTime: '2025-10-01T14:05:00Z', syncInProgress: false } } ``` ## Metadata Fields All entities have optional metadata fields for offline support: - `_optimistic` (boolean) - True if not yet synced with server - `_localId` (string) - Temporary ID before server assigns real ID - `_version` (number) - Version counter for conflict detection ## Best Practices 1. **Always use optimistic updates** for write operations (create, update, delete) 2. **Use entity selectors** instead of accessing state directly 3. **Handle offline state** in your UI 4. **Show sync indicators** to users 5. **Test offline scenarios** thoroughly 6. **Use normalized state** for related entities 7. **Implement proper error handling** and rollback logic ## Testing Offline Behavior ### In Chrome DevTools: 1. Open DevTools → Network tab 2. Change "Online" dropdown to "Offline" 3. Try creating/updating activities 4. Go back "Online" 5. Watch automatic sync happen ### Programmatically: ```typescript // Simulate offline dispatch(setOnlineStatus(false)); // Simulate online dispatch(setOnlineStatus(true)); ``` ## Performance Considerations - **IndexedDB** used for persistence (faster than localStorage) - **Entity adapters** provide memoized selectors - **Normalized state** prevents duplication - **Selective persistence** (blacklist for sensitive data) - **Batched updates** when syncing multiple actions ## Security Notes - Sensitive data can be excluded from persistence using `persistOptions.blacklist` - Auth tokens should NOT be stored in Redux (use secure cookie or AuthContext) - Offline queue is cleared on logout - All API calls still require valid authentication ## Troubleshooting ### Actions not being queued - Check if action type includes '/create', '/update', or '/delete' - Ensure `meta.requestId` exists on the action ### Sync not happening - Check browser console for network errors - Verify API endpoints are correct - Check if `isOnline` state is accurate ### State not persisting - Check browser IndexedDB (DevTools → Application → IndexedDB) - Verify localforage is configured correctly - Check for storage quota issues ## Future Enhancements - [ ] Batch sync for multiple actions - [ ] Conflict UI for user resolution - [ ] Sync progress indicators - [ ] Selective sync (by entity type) - [ ] Background sync (Service Workers) - [ ] Delta sync (only changed fields)