Files
maternal-app/maternal-web/store/README.md
Andrei 7cb2ff97de
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
feat: Implement offline-first Redux architecture with optimistic updates
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>
2025-10-01 19:24:46 +00:00

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)