WellNuo/hooks/useLoadingState.ts
Sergei 2b36f801f1 Add comprehensive loading state management system
- Add useLoadingState hook with data/loading/error state management
- Add useSimpleLoading hook for basic boolean loading state
- Add useMultipleLoadingStates hook for tracking multiple items
- Create Skeleton component with shimmer animation for placeholders
- Create specialized skeletons: SkeletonText, SkeletonAvatar, SkeletonCard,
  SkeletonListItem, SkeletonBeneficiaryCard, SkeletonSensorCard
- Create LoadingOverlay components: modal, inline, and card variants
- Create ScreenLoading wrapper with loading/error/content states
- Create RefreshableScreen with built-in pull-to-refresh
- Create EmptyState and LoadingButtonState utility components
- Add comprehensive tests for all components and hooks (61 tests passing)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-01 10:11:14 -08:00

198 lines
5.0 KiB
TypeScript

import { useState, useCallback } from 'react';
interface LoadingState<T> {
data: T | null;
isLoading: boolean;
error: string | null;
isRefreshing: boolean;
}
interface UseLoadingStateOptions<T> {
initialData?: T | null;
}
interface UseLoadingStateReturn<T> extends LoadingState<T> {
setData: (data: T | null) => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
setRefreshing: (refreshing: boolean) => void;
reset: () => void;
execute: <R = T>(
asyncFn: () => Promise<R>,
options?: { refresh?: boolean; transform?: (result: R) => T }
) => Promise<R | undefined>;
}
/**
* Custom hook for managing loading states
* Provides consistent loading, error, and data state management
*/
export function useLoadingState<T>(
options: UseLoadingStateOptions<T> = {}
): UseLoadingStateReturn<T> {
const { initialData = null } = options;
const [state, setState] = useState<LoadingState<T>>({
data: initialData,
isLoading: false,
error: null,
isRefreshing: false,
});
const setData = useCallback((data: T | null) => {
setState((prev) => ({ ...prev, data, error: null }));
}, []);
const setLoading = useCallback((isLoading: boolean) => {
setState((prev) => ({ ...prev, isLoading }));
}, []);
const setError = useCallback((error: string | null) => {
setState((prev) => ({ ...prev, error, isLoading: false, isRefreshing: false }));
}, []);
const setRefreshing = useCallback((isRefreshing: boolean) => {
setState((prev) => ({ ...prev, isRefreshing }));
}, []);
const reset = useCallback(() => {
setState({
data: initialData,
isLoading: false,
error: null,
isRefreshing: false,
});
}, [initialData]);
/**
* Execute an async function with automatic loading state management
* @param asyncFn - The async function to execute
* @param options.refresh - If true, sets isRefreshing instead of isLoading
* @param options.transform - Optional function to transform the result before setting data
*/
const execute = useCallback(
async <R = T>(
asyncFn: () => Promise<R>,
execOptions?: { refresh?: boolean; transform?: (result: R) => T }
): Promise<R | undefined> => {
const { refresh = false, transform } = execOptions || {};
try {
if (refresh) {
setState((prev) => ({ ...prev, isRefreshing: true, error: null }));
} else {
setState((prev) => ({ ...prev, isLoading: true, error: null }));
}
const result = await asyncFn();
// Transform and set data if transform function provided
if (transform) {
const transformedData = transform(result);
setState((prev) => ({
...prev,
data: transformedData,
isLoading: false,
isRefreshing: false,
}));
} else {
// If no transform, try to set result as data (if types match)
setState((prev) => ({
...prev,
data: result as unknown as T,
isLoading: false,
isRefreshing: false,
}));
}
return result;
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'An error occurred';
setState((prev) => ({
...prev,
error: errorMessage,
isLoading: false,
isRefreshing: false,
}));
return undefined;
}
},
[]
);
return {
...state,
setData,
setLoading,
setError,
setRefreshing,
reset,
execute,
};
}
/**
* Simple boolean loading state hook
* For cases where you just need a loading flag
*/
export function useSimpleLoading(initialState = false) {
const [isLoading, setIsLoading] = useState(initialState);
const startLoading = useCallback(() => setIsLoading(true), []);
const stopLoading = useCallback(() => setIsLoading(false), []);
const toggleLoading = useCallback(() => setIsLoading((prev) => !prev), []);
const withLoading = useCallback(
async <T>(asyncFn: () => Promise<T>): Promise<T> => {
setIsLoading(true);
try {
return await asyncFn();
} finally {
setIsLoading(false);
}
},
[]
);
return {
isLoading,
setIsLoading,
startLoading,
stopLoading,
toggleLoading,
withLoading,
};
}
/**
* Hook for managing multiple loading states by key
* Useful for list items that can have individual loading states
*/
export function useMultipleLoadingStates() {
const [loadingStates, setLoadingStates] = useState<Record<string, boolean>>({});
const setLoading = useCallback((key: string, loading: boolean) => {
setLoadingStates((prev) => ({ ...prev, [key]: loading }));
}, []);
const isLoading = useCallback(
(key: string) => loadingStates[key] || false,
[loadingStates]
);
const anyLoading = Object.values(loadingStates).some(Boolean);
const clearAll = useCallback(() => {
setLoadingStates({});
}, []);
return {
loadingStates,
setLoading,
isLoading,
anyLoading,
clearAll,
};
}