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>
105 lines
3.3 KiB
TypeScript
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;
|