/** * Offline-Aware Data Fetching Hook * * Custom hook that handles data fetching with offline detection, * loading states, error handling, and retry logic. */ import { useState, useEffect, useCallback } from 'react'; import { useNetworkStatus } from '@/utils/networkStatus'; import type { ApiResponse, ApiError } from '@/types'; import { isNetworkError, getNetworkErrorMessage } from '@/services/offlineAwareApi'; interface UseOfflineAwareDataOptions { /** * Skip fetching data initially (manual fetch only) * Default: false */ skip?: boolean; /** * Refetch data when network comes back online * Default: true */ refetchOnReconnect?: boolean; /** * Custom error message when offline */ offlineMessage?: string; /** * Poll interval in milliseconds (0 to disable) * Default: 0 */ pollInterval?: number; } interface UseOfflineAwareDataReturn { /** * Fetched data (null if not loaded or error) */ data: T | null; /** * Loading state (true during initial fetch) */ loading: boolean; /** * Error object (null if no error) */ error: ApiError | null; /** * Refetch data manually */ refetch: () => Promise; /** * Is currently refetching (true during manual refetch) */ refetching: boolean; /** * User-friendly error message */ errorMessage: string | null; /** * Is the error network-related? */ isOfflineError: boolean; } /** * Custom hook for offline-aware data fetching * * @param fetcher - Async function that returns ApiResponse * @param dependencies - Dependencies array (like useEffect) * @param options - Configuration options * * @example * function MyComponent() { * const { data, loading, error, refetch } = useOfflineAwareData( * () => offlineAwareApi.getAllBeneficiaries(), * [] * ); * * if (loading) return ; * if (error) return ; * return ; * } */ export function useOfflineAwareData( fetcher: () => Promise>, dependencies: any[] = [], options: UseOfflineAwareDataOptions = {} ): UseOfflineAwareDataReturn { const { skip = false, refetchOnReconnect = true, offlineMessage, pollInterval = 0, } = options; const [data, setData] = useState(null); const [loading, setLoading] = useState(!skip); const [refetching, setRefetching] = useState(false); const [error, setError] = useState(null); const { isOnline } = useNetworkStatus(); // Fetch data const fetchData = useCallback( async (isRefetch = false) => { if (isRefetch) { setRefetching(true); } else { setLoading(true); } setError(null); try { const response = await fetcher(); if (response.ok && response.data) { setData(response.data); setError(null); } else { setData(null); setError(response.error || { message: 'Unknown error' }); } } catch (err) { setData(null); setError({ message: err instanceof Error ? err.message : 'Unknown error', code: 'EXCEPTION', }); } finally { setLoading(false); setRefetching(false); } }, [fetcher] ); // Initial fetch useEffect(() => { if (!skip) { fetchData(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, dependencies); // Refetch when network comes back online useEffect(() => { if (refetchOnReconnect && isOnline && error && isNetworkError(error)) { fetchData(true); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOnline, refetchOnReconnect, error]); // Polling (if enabled) useEffect(() => { if (pollInterval > 0 && !skip) { const interval = setInterval(() => { if (isOnline) { fetchData(true); } }, pollInterval); return () => clearInterval(interval); } }, [pollInterval, skip, isOnline, fetchData]); // Manual refetch const refetch = useCallback(async () => { await fetchData(true); }, [fetchData]); // Computed values const isOfflineError = error ? isNetworkError(error) : false; const errorMessage = error ? offlineMessage && isOfflineError ? offlineMessage : getNetworkErrorMessage(error) : null; return { data, loading, error, refetch, refetching, errorMessage, isOfflineError, }; } /** * Simpler hook for offline-aware mutations (create, update, delete) * * @example * function MyComponent() { * const { mutate, loading, error } = useOfflineAwareMutation( * (data) => offlineAwareApi.createBeneficiary(data) * ); * * const handleSubmit = async () => { * const result = await mutate(formData); * if (result.ok) { * // Success * } * }; * } */ export function useOfflineAwareMutation( mutationFn: (variables: TVariables) => Promise>, options: { offlineMessage?: string; onSuccess?: (data: TData) => void; onError?: (error: ApiError) => void; } = {} ) { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const { isOnline } = useNetworkStatus(); const mutate = useCallback( async (variables: TVariables): Promise> => { setLoading(true); setError(null); try { const response = await mutationFn(variables); if (response.ok && response.data) { options.onSuccess?.(response.data); } else { setError(response.error || { message: 'Unknown error' }); options.onError?.(response.error || { message: 'Unknown error' }); } return response; } catch (err) { const apiError: ApiError = { message: err instanceof Error ? err.message : 'Unknown error', code: 'EXCEPTION', }; setError(apiError); options.onError?.(apiError); return { ok: false, error: apiError }; } finally { setLoading(false); } }, // eslint-disable-next-line react-hooks/exhaustive-deps [mutationFn, options.onSuccess, options.onError] ); const isOfflineError = error ? isNetworkError(error) : false; const errorMessage = error ? options.offlineMessage && isOfflineError ? options.offlineMessage : getNetworkErrorMessage(error) : null; return { mutate, loading, error, errorMessage, isOfflineError, isOnline, }; }