Files
maternal-app/maternal-web/store/store.ts
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

105 lines
3.3 KiB
TypeScript

import { configureStore, Middleware } from '@reduxjs/toolkit';
import { offline } from '@redux-offline/redux-offline';
import offlineConfig from '@redux-offline/redux-offline/lib/defaults';
import localforage from 'localforage';
// Slices
import offlineReducer from './slices/offlineSlice';
import activitiesReducer from './slices/activitiesSlice';
import childrenReducer from './slices/childrenSlice';
import networkReducer from './slices/networkSlice';
// Middleware
import { offlineMiddleware } from './middleware/offlineMiddleware';
import { syncMiddleware } from './middleware/syncMiddleware';
// Configure localforage for IndexedDB storage
localforage.config({
name: 'maternal-app',
storeName: 'offline_data',
driver: [localforage.INDEXEDDB, localforage.LOCALSTORAGE],
});
// Custom offline configuration
const customOfflineConfig = {
...offlineConfig,
persistOptions: {
blacklist: ['_persist'], // Don't persist the persist state
},
// Effect function - how to execute side effects
effect: async (effect: any, action: any) => {
const { url, method = 'GET', body, headers = {} } = effect;
try {
const response = await fetch(url, {
method,
body: body ? JSON.stringify(body) : undefined,
headers: {
'Content-Type': 'application/json',
...headers,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
throw error;
}
},
// Discard function - when to discard failed actions
discard: (error: any, action: any, retries: number) => {
// Discard after 5 retries or if it's a 4xx error
const is4xxError = error.message?.includes('HTTP 4');
return retries >= 5 || is4xxError;
},
// Retry function - calculate retry delay with exponential backoff
retry: (action: any, retries: number) => {
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
return Math.min(1000 * Math.pow(2, retries), 16000);
},
persistCallback: () => {
console.log('[Redux Offline] State persisted to storage');
},
persistAutoRehydrate: true,
};
export const store = configureStore({
reducer: {
offline: offlineReducer,
activities: activitiesReducer,
children: childrenReducer,
network: networkReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
// Ignore these action types for serialization check
ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'],
// Ignore these field paths in all actions
ignoredActionPaths: ['meta.arg', 'payload.timestamp'],
// Ignore these paths in the state
ignoredPaths: ['items.dates'],
},
}).concat(
offlineMiddleware as Middleware,
syncMiddleware as Middleware,
// Add redux-offline middleware
offline(customOfflineConfig).middleware as Middleware
),
enhancers: (getDefaultEnhancers) =>
getDefaultEnhancers().concat(
// Add redux-offline enhancer
offline(customOfflineConfig).enhancer
),
});
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// Export store instance
export default store;