Changed network detection to only mark as offline on actual network errors, not on HTTP errors like 404. This fixes the issue where the app shows 'You are offline' even when connected, which happens when accessing through a reverse proxy where the /api/health endpoint might not be properly routed. Now the app will show as online as long as it can reach the server (any HTTP response), and only show offline on true connection failures. 🤖 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)