- Install redux-persist package - Configure persistReducer with whitelist (offline, activities, children) - Exclude network slice from persistence (should be fresh on reload) - Add PersistGate to ReduxProvider with loading indicator - Configure serializableCheck to ignore persist actions - Store state now persists to localStorage automatically This fixes the issue where app state was lost on page reload, improving UX. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
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:
// 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 precedenceCLIENT_WINS- Local data takes precedenceLAST_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
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
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
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
import { useIsOptimistic } from '@/store/hooks';
function ActivityCard({ activityId }) {
const isOptimistic = useIsOptimistic('activities', activityId);
return (
<Card>
{isOptimistic && (
<Chip label="Syncing..." size="small" />
)}
{/* ... */}
</Card>
);
}
State Shape
{
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
- Always use optimistic updates for write operations (create, update, delete)
- Use entity selectors instead of accessing state directly
- Handle offline state in your UI
- Show sync indicators to users
- Test offline scenarios thoroughly
- Use normalized state for related entities
- Implement proper error handling and rollback logic
Testing Offline Behavior
In Chrome DevTools:
- Open DevTools → Network tab
- Change "Online" dropdown to "Offline"
- Try creating/updating activities
- Go back "Online"
- Watch automatic sync happen
Programmatically:
// 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.requestIdexists on the action
Sync not happening
- Check browser console for network errors
- Verify API endpoints are correct
- Check if
isOnlinestate 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)