Files
maternal-app/maternal-web/store
Andrei 2747630013
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: Update Redux children slice for multi-child support
State Updates:
- Added selectedChildIds array for multi-select
- Added defaultChildId for quick actions
- Added viewMode (auto/tabs/cards) with automatic detection
- Added lastSelectedPerScreen for per-route child memory
- Updated Child interface with displayColor, sortOrder, nickname fields
- Changed sort comparator to use sortOrder (birth order) instead of createdAt

New Actions:
- selectChildren(ids[]) - Select multiple children
- toggleChildSelection(id) - Toggle single child in multi-select
- setDefaultChild(id) - Set default child for quick actions
- setViewMode(mode) - Manual view mode override
- setLastSelectedForScreen({screen, childId}) - Remember per-screen selection

localStorage Integration:
- Persists selectedChildId
- Persists defaultChildId
- Persists viewMode preference
- Persists lastSelectedPerScreen map

New Selectors:
- selectSelectedChildren() - Get all selected children as array
- selectDefaultChild() - Get default child entity
- selectChildrenCount() - Total number of children
- selectViewMode() - Computed view mode (tabs/cards based on count)
- selectChildColor(childId) - Get child's display color
- selectLastSelectedForScreen(screen) - Get last child for specific screen

View Mode Logic:
- auto + <=3 children = tabs
- auto + >3 children = cards
- manual override = use set value

Use Cases:
- Dashboard child switching with tabs/cards
- Multi-child activity logging
- Child-specific routing memory
- Default child for quick actions
- Color-coded UI elements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 21:35:34 +00:00
..

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)