Skip to main content

Overview

BodyWorks provides a comprehensive set of custom React hooks built on top of React Query (TanStack Query) for efficient data fetching, caching, and state management.

Hook Categories

Data Fetching

Hooks for fetching exercises, routines, body parts, equipment, and target muscles

Single Resource

Hooks for fetching individual items by ID

Utilities

Device detection and other utility hooks

Data Fetching Hooks

All data fetching hooks use React Query for caching, automatic refetching, and optimistic updates.

useExercises

Fetches a paginated list of exercises.
import useExercises from "@/hooks/useExercises";

function ExerciseList() {
  const { exercises, isLoading, error, refetch } = useExercises(9, 1);

  if (isLoading) return <Skeleton />;
  if (error) return <div>Error loading exercises</div>;

  return (
    <div>
      {exercises?.data.map((exercise) => (
        <ExerciseCard key={exercise.id} {...exercise} />
      ))}
    </div>
  );
}
The keepPreviousData option ensures smooth pagination by showing previous data while fetching new pages.

useRoutines

Fetches a paginated list of workout routines.
import { useRoutines } from "@/hooks/useRoutines";

function RoutineList() {
  const { routines, isLoading, error } = useRoutines(6, 1);

  return (
    <div className="grid grid-cols-2 gap-4">
      {routines?.data.map((routine) => (
        <RoutineCard key={routine.id} {...routine} />
      ))}
    </div>
  );
}
Parameters:
  • limit: number - Number of routines per page
  • page: number - Current page number
Returns:
{
  isLoading: boolean;
  routines: IRoutinesResponse;
  error: Error | null;
  refetch: () => void;
  isRefetching: boolean;
}

useBodyParts

Fetches all body parts with optional limit.
import { useBodyParts } from "@/hooks/useBodyParts";

function BodyPartFilter() {
  const { bodyParts, isLoading } = useBodyParts(20);

  return (
    <select>
      {bodyParts?.data.map((part) => (
        <option key={part.id} value={part.id}>
          {part.name}
        </option>
      ))}
    </select>
  );
}

useEquipments

Fetches all available equipment.
import { useEquipments } from "@/hooks/useEquipments";

function EquipmentFilter() {
  const { equipments, isLoading } = useEquipments();

  return (
    <div className="flex gap-2">
      {equipments?.data.map((equipment) => (
        <Button key={equipment.id} variant="outline">
          {equipment.name}
        </Button>
      ))}
    </div>
  );
}

useTargetMuscles

Fetches all target muscles with optional limit.
import { useTargetMuscles } from "@/hooks/useTargetMuscles";

function MuscleGroupFilter() {
  const { targetMuscles, isLoading } = useTargetMuscles(15);

  return (
    <div className="grid grid-cols-3 gap-2">
      {targetMuscles?.data.map((muscle) => (
        <Card key={muscle.id}>
          <h3>{muscle.name}</h3>
        </Card>
      ))}
    </div>
  );
}

Single Resource Hooks

Hooks for fetching individual items by ID.

useExercise

Fetches a single exercise by ID.
import { useExercise } from "@/hooks/useExercise";
import { useParams } from "next/navigation";

function ExerciseDetail() {
  const params = useParams();
  const { exercise, isLoading, error } = useExercise(params.id as string);

  if (isLoading) return <Skeleton className="h-96" />;
  if (error) {
    toast.error("Failed to load exercise");
    return null;
  }

  return (
    <div>
      <h1>{exercise?.name}</h1>
      <img src={exercise?.gifUrl} alt={exercise?.name} />
      <p>{exercise?.instructions}</p>
    </div>
  );
}

useRoutine

Fetches a single routine by ID.
import { useRoutine } from "@/hooks/useRoutine";

function RoutineDetail({ routineId }: { routineId: string }) {
  const { routine, isLoading, error, refetch } = useRoutine(routineId);

  return (
    <div>
      <h1>{routine?.title}</h1>
      <p>{routine?.description}</p>
      <Button onClick={() => refetch()}>Refresh</Button>
    </div>
  );
}

useBodyPart

Fetches a single body part by ID.
import { useBodyPart } from "@/hooks/useBodyPart";

function BodyPartDetail({ bodyPartId }: { bodyPartId: string }) {
  const { bodyPart, isLoading } = useBodyPart(bodyPartId);

  return <h2>{bodyPart?.name}</h2>;
}

useEquipment

Fetches a single equipment item by ID.
import { useEquipment } from "@/hooks/useEquipment";

function EquipmentDetail({ equipmentId }: { equipmentId: string }) {
  const { equipment, isLoading } = useEquipment(equipmentId);

  return <div>{equipment?.name}</div>;
}

useTargetMuscle

Fetches a single target muscle by ID.
import { useTargetMuscle } from "@/hooks/useTargetMuscle";

function TargetMuscleDetail({ muscleId }: { muscleId: string }) {
  const { targetMuscle, isLoading } = useTargetMuscle(muscleId);

  return <h3>{targetMuscle?.name}</h3>;
}

useRoutinesCategory

Fetches routines by category.
import { useRoutinesCategory } from "@/hooks/useRoutinesCategory";

function CategoryRoutines({ category }: { category: string }) {
  const { routines, isLoading } = useRoutinesCategory(category);

  return (
    <div>
      {routines?.data.map((routine) => (
        <RoutineCard key={routine.id} {...routine} />
      ))}
    </div>
  );
}

Utility Hooks

General-purpose utility hooks.

useDevice

Detects device type and custom media queries.
import useDevice from "@/hooks/useDevice";

function ResponsiveComponent() {
  const { isMobile, isTablet, isDesktop, isDesktopLarge } = useDevice();

  return (
    <div>
      {isMobile && <MobileNav />}
      {isTablet && <TabletNav />}
      {isDesktop && <DesktopNav />}
      {isDesktopLarge && <LargeDesktopNav />}
    </div>
  );
}
The useDevice hook returns false for all queries during SSR. Ensure your components handle this gracefully to avoid hydration mismatches.

Hook Patterns

Common patterns when using BodyWorks hooks.

Pagination Pattern

import useExercises from "@/hooks/useExercises";
import { Pagination } from "@/components/ui/pagination";
import { useState } from "react";

function ExercisePagination() {
  const [page, setPage] = useState(1);
  const limit = 9;
  const { exercises, isLoading, isRefetching } = useExercises(limit, page);

  return (
    <div>
      {(isLoading || isRefetching) && <Skeleton />}
      
      <div className="grid grid-cols-3 gap-4">
        {exercises?.data.map((exercise) => (
          <ExerciseCard key={exercise.id} {...exercise} />
        ))}
      </div>

      <Pagination
        currentPage={page}
        totalPages={exercises?.pagination.totalPages}
        onPageChange={setPage}
      />
    </div>
  );
}

Combined Filters Pattern

import useExercises from "@/hooks/useExercises";
import { useBodyParts } from "@/hooks/useBodyParts";
import { useEquipments } from "@/hooks/useEquipments";

function ExerciseFilters() {
  const { bodyParts } = useBodyParts();
  const { equipments } = useEquipments();
  const { exercises, refetch } = useExercises();

  const [selectedBodyPart, setSelectedBodyPart] = useState("");
  const [selectedEquipment, setSelectedEquipment] = useState("");

  useEffect(() => {
    refetch();
  }, [selectedBodyPart, selectedEquipment]);

  return (
    <div>
      <select onChange={(e) => setSelectedBodyPart(e.target.value)}>
        {bodyParts?.data.map((part) => (
          <option key={part.id} value={part.id}>{part.name}</option>
        ))}
      </select>

      <select onChange={(e) => setSelectedEquipment(e.target.value)}>
        {equipments?.data.map((eq) => (
          <option key={eq.id} value={eq.id}>{eq.name}</option>
        ))}
      </select>
    </div>
  );
}

Search with Debounce Pattern

import useExercises from "@/hooks/useExercises";
import { SearchBar } from "@/components/search-bar";
import { useState } from "react";

function SearchableExercises() {
  const [searchQuery, setSearchQuery] = useState("");
  const { exercises, isLoading } = useExercises();

  const filteredExercises = exercises?.data.filter((exercise) =>
    exercise.name.toLowerCase().includes(searchQuery.toLowerCase())
  );

  return (
    <div>
      <SearchBar getQuery={setSearchQuery} />
      
      {isLoading ? (
        <Skeleton />
      ) : (
        <div className="grid grid-cols-3 gap-4">
          {filteredExercises?.map((exercise) => (
            <ExerciseCard key={exercise.id} {...exercise} />
          ))}
        </div>
      )}
    </div>
  );
}

Conditional Rendering Pattern

import { useExercise } from "@/hooks/useExercise";
import useDevice from "@/hooks/useDevice";

function ExerciseDetailPage({ exerciseId }: { exerciseId: string }) {
  const { exercise, isLoading, error } = useExercise(exerciseId);
  const { isMobile } = useDevice();

  if (isLoading) return <Skeleton className="h-screen" />;
  
  if (error) {
    return (
      <div className="text-center p-8">
        <h2>Failed to load exercise</h2>
        <Button onClick={() => window.location.reload()}>
          Try Again
        </Button>
      </div>
    );
  }

  return (
    <div className={isMobile ? "px-4" : "px-8"}>
      <h1>{exercise?.name}</h1>
      {/* Exercise details */}
    </div>
  );
}

React Query Configuration

All data hooks use React Query with these defaults:
// QueryClient configuration
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,     // 5 minutes
      cacheTime: 10 * 60 * 1000,    // 10 minutes
      refetchOnWindowFocus: false,   // Don't refetch on window focus
      retry: 3,                      // Retry failed requests 3 times
    },
  },
});

Query Keys

Hooks use consistent query key patterns:
// List queries
["exercises", limit, page]
["routines", limit, page]
["body-parts", limit]

// Single resource queries
["exercise", exerciseId]
["routine", routineId]
["body-part", bodyPartId]
Query keys are used for caching and invalidation. Use the same key pattern when manually invalidating queries:
queryClient.invalidateQueries(["exercises"]);

Best Practices

const { data, isLoading, error } = useExercises();

if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage />;

return <DataDisplay data={data} />;
const { exercises, isRefetching } = useExercises();

return (
  <div>
    {isRefetching && <LoadingSpinner />}
    <ExerciseList data={exercises} />
  </div>
);
const { exercises } = useExercises();

const sortedExercises = useMemo(
  () => exercises?.data.sort((a, b) => a.name.localeCompare(b.name)),
  [exercises]
);
All paginated hooks already use keepPreviousData to prevent flickering during page transitions.

Next Steps

API Reference

Explore the backend API endpoints

Components

Learn about UI components and theming

Build docs developers (and LLMs) love