# 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)