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

7.8 KiB

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

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

  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:

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