- 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>
198 lines
5.0 KiB
TypeScript
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,
|
|
};
|
|
}
|