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>
94 lines
2.7 KiB
TypeScript
94 lines
2.7 KiB
TypeScript
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;
|
|
});
|
|
};
|