Overview
useExercises is a custom React hook built on React Query that fetches paginated exercise data from the BodyWorks API. It provides automatic caching, loading states, error handling, and seamless pagination with placeholder data support.
Hook Signature
const useExercises = (
limit: number = 9,
page: number = 1
) => {
const {
isLoading,
data: exercises,
error,
refetch,
isRefetching,
} = useQuery({
queryKey: ["exercises", limit, page],
queryFn: () => getExercises(limit, page),
placeholderData: keepPreviousData,
});
return { isLoading, exercises, error, refetch, isRefetching };
};
Parameters
The number of exercises to fetch per page. Controls the pagination size.
The page number to fetch. Must be a positive integer starting from 1.
Return Values
The hook returns an object with the following properties:
Indicates whether the initial data is being loaded. true during the first fetch, false once data is available or an error occurs.
exercises
IExerciseData | undefined
The fetched exercise data object containing:
totalExercises (number): Total number of exercises available
totalPages (number): Total number of pages based on the limit
data (IExercise[]): Array of exercise objects
Contains error information if the request fails, otherwise null.
refetch
() => Promise<QueryObserverResult>
Function to manually refetch the exercise data. Useful for implementing refresh functionality.
Indicates whether the data is being refetched. true during background updates, false otherwise.
Exercise Data Structure
interface IExercise {
name: string; // Exercise name identifier
title: string; // Display title
target: string; // Target muscle group
muscles_worked: string; // Description of muscles worked
bodyPart: string; // Primary body part
equipment: string; // Required equipment
id: string; // Unique identifier
id_: string; // Alternative identifier
blog: string; // Related blog content
images: string[]; // Exercise images
gifUrl: string; // Animated demonstration GIF
videos: string[]; // Video tutorials
keywords: string[]; // Search keywords
}
interface IExerciseData {
totalExercises: number;
totalPages: number;
data: IExercise[];
}
Usage Examples
Basic Usage
With Refetch
Custom Page Size
import useExercises from '@/hooks/useExercises';
function ExerciseList() {
const { isLoading, exercises, error } = useExercises();
if (isLoading) return <div>Loading exercises...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h2>Total Exercises: {exercises?.totalExercises}</h2>
<div className="grid">
{exercises?.data.map((exercise) => (
<div key={exercise.id}>
<h3>{exercise.title}</h3>
<p>Target: {exercise.target}</p>
<p>Equipment: {exercise.equipment}</p>
</div>
))}
</div>
</div>
);
}
import useExercises from '@/hooks/useExercises';
function ExercisesWithRefresh() {
const { isLoading, exercises, error, refetch, isRefetching } = useExercises(10, 1);
const handleRefresh = async () => {
await refetch();
};
return (
<div>
<div className="header">
<h2>Exercise Database</h2>
<button
onClick={handleRefresh}
disabled={isRefetching}
>
{isRefetching ? 'Refreshing...' : 'Refresh'}
</button>
</div>
{isLoading ? (
<LoadingSkeleton />
) : error ? (
<ErrorMessage error={error} onRetry={refetch} />
) : (
<ExerciseGrid exercises={exercises?.data} />
)}
</div>
);
}
import { useState } from 'react';
import useExercises from '@/hooks/useExercises';
function CustomPageSize() {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(9);
const { isLoading, exercises, error } = useExercises(pageSize, page);
const handlePageSizeChange = (newSize: number) => {
setPageSize(newSize);
setPage(1); // Reset to first page
};
return (
<div>
<div className="controls">
<label>Items per page:</label>
<select
value={pageSize}
onChange={(e) => handlePageSizeChange(Number(e.target.value))}
>
<option value={6}>6</option>
<option value={9}>9</option>
<option value={12}>12</option>
<option value={24}>24</option>
</select>
</div>
<div className="results">
{exercises?.data.map((exercise) => (
<ExerciseCard key={exercise.id} exercise={exercise} />
))}
</div>
</div>
);
}
React Query Features
Automatic Caching
The hook uses React Query’s caching mechanism with a query key based on both limit and page parameters:
queryKey: ["exercises", limit, page]
This ensures that each unique combination of pagination parameters is cached separately, reducing unnecessary API calls.
Placeholder Data
The hook uses keepPreviousData to maintain smooth pagination:
placeholderData: keepPreviousData
With keepPreviousData, when navigating between pages, the previous page’s data remains visible until the new data loads. This prevents layout shifts and provides a better user experience.
Background Refetching
React Query automatically refetches data in the background when:
- The window regains focus
- The network reconnects
- A refetch interval is configured (not set by default)
Use the isRefetching flag to show loading indicators during background updates without hiding the current data.
Best Practices
Optimize pagination: Choose a limit value that balances performance with user experience. Typical values range from 9-24 items per page.
Handle loading states: Always check isLoading before accessing exercises?.data to prevent runtime errors.
Error boundaries: Wrap components using this hook in error boundaries to gracefully handle API failures.
Prefetch next page: For better UX, consider prefetching the next page when users reach the bottom of the current page using React Query’s prefetchQuery.
The hook returns undefined for exercises during the initial load. Always use optional chaining (exercises?.data) when accessing nested properties.
Common Patterns
Loading States
const { isLoading, exercises, isRefetching } = useExercises();
if (isLoading) {
return <LoadingSpinner />; // Initial load
}
return (
<div>
{isRefetching && <RefreshIndicator />} {/* Background update */}
{/* Content */}
</div>
);
Error Handling
const { error, refetch } = useExercises();
if (error) {
return (
<div>
<p>Failed to load exercises: {error.message}</p>
<button onClick={() => refetch()}>Try Again</button>
</div>
);
}
function InfiniteExercises() {
const [page, setPage] = useState(1);
const [allExercises, setAllExercises] = useState<IExercise[]>([]);
const { exercises, isLoading } = useExercises(12, page);
useEffect(() => {
if (exercises?.data) {
setAllExercises(prev => [...prev, ...exercises.data]);
}
}, [exercises]);
const loadMore = () => setPage(prev => prev + 1);
return (
<div>
{allExercises.map(exercise => (
<ExerciseCard key={exercise.id} exercise={exercise} />
))}
{page < (exercises?.totalPages || 0) && (
<button onClick={loadMore} disabled={isLoading}>
Load More
</button>
)}
</div>
);
}