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>
This commit is contained in:
93
maternal-web/store/hooks.ts
Normal file
93
maternal-web/store/hooks.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
|
||||
import type { RootState, AppDispatch } from './store';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
|
||||
// Custom hooks for common patterns
|
||||
|
||||
/**
|
||||
* Hook to check if the app is online
|
||||
*/
|
||||
export const useIsOnline = () => {
|
||||
return useAppSelector((state) => state.network?.isOnline ?? true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to check if there are pending sync actions
|
||||
*/
|
||||
export const useHasPendingSync = () => {
|
||||
const pendingActions = useAppSelector((state) => state.offline?.pendingActions ?? []);
|
||||
return pendingActions.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get sync status
|
||||
*/
|
||||
export const useSyncStatus = () => {
|
||||
const syncInProgress = useAppSelector((state) => state.offline?.syncInProgress ?? false);
|
||||
const lastSyncTime = useAppSelector((state) => state.offline?.lastSyncTime);
|
||||
const pendingCount = useAppSelector((state) => state.offline?.pendingActions?.length ?? 0);
|
||||
|
||||
return {
|
||||
syncing: syncInProgress,
|
||||
lastSync: lastSyncTime,
|
||||
pendingCount,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for optimistic updates
|
||||
* Provides a function that dispatches both optimistic and actual actions
|
||||
*/
|
||||
export const useOptimisticAction = <T extends any[], R>(
|
||||
optimisticAction: (...args: T) => any,
|
||||
actualAction: (...args: T) => any
|
||||
) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isOnline = useIsOnline();
|
||||
|
||||
return useCallback(
|
||||
async (...args: T) => {
|
||||
// Always dispatch optimistic action for immediate UI update
|
||||
dispatch(optimisticAction(...args));
|
||||
|
||||
// If online, dispatch actual action
|
||||
if (isOnline) {
|
||||
try {
|
||||
const result = await dispatch(actualAction(...args));
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Rollback will be handled by the rejected case in the slice
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// If offline, the action will be queued by the middleware
|
||||
return Promise.resolve();
|
||||
},
|
||||
[dispatch, optimisticAction, actualAction, isOnline]
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get network quality information
|
||||
*/
|
||||
export const useNetworkQuality = () => {
|
||||
const quality = useAppSelector((state) => state.network?.connectionQuality ?? 'excellent');
|
||||
const latency = useAppSelector((state) => state.network?.latency);
|
||||
|
||||
return { quality, latency };
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to check if a specific entity is being optimistically updated
|
||||
*/
|
||||
export const useIsOptimistic = (entityType: 'activities' | 'children', id: string) => {
|
||||
return useAppSelector((state) => {
|
||||
const entity = state[entityType]?.entities?.[id];
|
||||
return entity?._optimistic ?? false;
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user