Implemented comprehensive offline-first state management: Redux Store Setup: - Configure Redux Toolkit with @redux-offline/redux-offline - Setup redux-persist with IndexedDB (localforage) - Custom offline config with exponential backoff retry - Normalized state with entity adapters State Slices: - activitiesSlice: Normalized activities with optimistic CRUD - childrenSlice: Normalized children with optimistic CRUD - networkSlice: Network status and connection quality - offlineSlice: Sync queue and pending actions Middleware: - offlineMiddleware: Queue actions when offline - syncMiddleware: Process pending actions when online - Conflict resolution strategies (SERVER_WINS, CLIENT_WINS, LAST_WRITE_WINS, MERGE) - Version-based conflict detection Features: - Optimistic updates for immediate UI feedback - Automatic sync queue with retry logic (5 retries max) - Network detection (browser events + periodic checks) - Connection quality monitoring (excellent/good/poor/offline) - Latency tracking - Conflict resolution with multiple strategies - Entity versioning for optimistic updates Components: - NetworkStatusIndicator: Full-screen status banner - NetworkStatusBadge: Compact app bar badge - ReduxProvider: Provider with network detection setup Custom Hooks: - useAppDispatch/useAppSelector: Typed Redux hooks - useIsOnline: Check online status - useHasPendingSync: Check for pending actions - useSyncStatus: Get sync progress info - useOptimisticAction: Combine optimistic + actual actions - useNetworkQuality: Get connection quality - useIsOptimistic: Check if entity is being synced Documentation: - Comprehensive README with usage examples - Architecture overview - Best practices guide - Troubleshooting section State Structure: - Normalized entities with byId/allIds - Optimistic metadata (_optimistic, _localId, _version) - Entity adapters with memoized selectors - Offline queue persistence to IndexedDB 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
287 lines
7.8 KiB
Markdown
287 lines
7.8 KiB
Markdown
# 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 (
|
|
<div>
|
|
{!isOnline && <p>You are offline. Changes will sync when you reconnect.</p>}
|
|
{/* ... */}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 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 (
|
|
<div>
|
|
{syncing && <p>Syncing {pendingCount} changes...</p>}
|
|
<button onClick={() => handleCreate(newActivity)}>
|
|
Add Activity
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Network Status Indicator
|
|
|
|
```typescript
|
|
import { NetworkStatusIndicator, NetworkStatusBadge } from '@/components/common/NetworkStatusIndicator';
|
|
|
|
function AppLayout() {
|
|
return (
|
|
<>
|
|
<NetworkStatusIndicator /> {/* Snackbar at top */}
|
|
<AppBar>
|
|
<NetworkStatusBadge /> {/* Small badge in app bar */}
|
|
</AppBar>
|
|
</>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Checking if Entity is Optimistic
|
|
|
|
```typescript
|
|
import { useIsOptimistic } from '@/store/hooks';
|
|
|
|
function ActivityCard({ activityId }) {
|
|
const isOptimistic = useIsOptimistic('activities', activityId);
|
|
|
|
return (
|
|
<Card>
|
|
{isOptimistic && (
|
|
<Chip label="Syncing..." size="small" />
|
|
)}
|
|
{/* ... */}
|
|
</Card>
|
|
);
|
|
}
|
|
```
|
|
|
|
## 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)
|