Skip to main content

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

limit
number
default:"9"
The number of exercises to fetch per page. Controls the pagination size.
page
number
default:"1"
The page number to fetch. Must be a positive integer starting from 1.

Return Values

The hook returns an object with the following properties:
isLoading
boolean
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
error
Error | null
Contains error information if the request fails, otherwise null.
refetch
() => Promise<QueryObserverResult>
Function to manually refetch the exercise data. Useful for implementing refresh functionality.
isRefetching
boolean
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

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>
  );
}

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>
  );
}

Infinite Scroll

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>
  );
}

Build docs developers (and LLMs) love