Skip to main content

Overview

useRoutines is a custom React hook built on React Query that fetches paginated workout routine data from the BodyWorks API. It provides automatic caching, loading states, error handling, and seamless pagination with placeholder data support.

Hook Signature

export const useRoutines = (
  limit: number,
  page: number
) => {
  const {
    isLoading,
    data: routines,
    error,
    refetch,
    isRefetching,
  } = useQuery({
    queryKey: ["routines", limit, page],
    queryFn: () => getRoutines(limit, page),
    placeholderData: keepPreviousData,
  });
  return { isLoading, routines, error, refetch, isRefetching };
};

Parameters

limit
number
required
The number of routines to fetch per page. Controls the pagination size.
page
number
required
The page number to fetch. Must be a positive integer starting from 1.
Unlike useExercises, both parameters are required and have no default values. You must explicitly specify the pagination parameters.

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.
routines
IRoutinesResponse | undefined
The fetched routine data object containing:
  • totalRoutines (number): Total number of routines available
  • totalPages (number): Total number of pages based on the limit
  • data (IRoutine[]): Array of routine objects
error
Error | null
Contains error information if the request fails, otherwise null.
refetch
() => Promise<QueryObserverResult>
Function to manually refetch the routine data. Useful for implementing refresh functionality.
isRefetching
boolean
Indicates whether the data is being refetched. true during background updates, false otherwise.

Routine Data Structure

interface IRoutine {
  category: string[];        // Routine categories
  routine: {
    routine_title: string;
    routine_description: string;
    routine_imageUrl: string;
    workout_plan: {
      heading: string;
      day_plan: string;
    }[];
    workout_summary: {
      MainGoal: string;
      WorkoutType: string;
      TrainingLevel: string;
      ProgramDuration: string;
      DaysPerWeek: number;
      TimePerWorkout: string;
      EquipmentRequired: string;
      TargetGender: string;
    };
  };
  id_: number;
  id: number;
}

interface IRoutinesResponse {
  totalPages: number;
  totalRoutines: number;
  data: IRoutine[];
}

Usage Examples

import { useRoutines } from '@/hooks/useRoutines';

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

  if (isLoading) return <div>Loading routines...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h2>Workout Routines ({routines?.totalRoutines})</h2>
      <div className="routines-grid">
        {routines?.data.map((routine) => (
          <div key={routine.id} className="routine-card">
            <img 
              src={routine.routine.routine_imageUrl} 
              alt={routine.routine.routine_title} 
            />
            <h3>{routine.routine.routine_title}</h3>
            <p>{routine.routine.routine_description}</p>
            <div className="tags">
              {routine.category.map(cat => (
                <span key={cat}>{cat}</span>
              ))}
            </div>
          </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: ["routines", limit, page]
Each unique combination of parameters is cached separately, preventing duplicate API calls when navigating between pages.

Placeholder Data

The hook uses keepPreviousData for smooth pagination transitions:
placeholderData: keepPreviousData
Previous page data remains visible while new data loads, preventing layout shifts and improving perceived performance.

Cache Invalidation

You can manually invalidate the cache when needed:
import { useQueryClient } from '@tanstack/react-query';
import { useRoutines } from '@/hooks/useRoutines';

function RoutineManager() {
  const queryClient = useQueryClient();
  const { routines } = useRoutines(10, 1);

  const handleRoutineUpdate = async () => {
    // After updating a routine
    await updateRoutine(/* ... */);
    
    // Invalidate all routine queries
    queryClient.invalidateQueries({ queryKey: ['routines'] });
  };

  // ...
}

Best Practices

Specify appropriate page sizes: For routine cards with images and detailed information, consider smaller page sizes (6-12 items) to maintain good performance.
Display workout summaries: Use the workout_summary data to help users quickly filter and find routines matching their goals and fitness level.
Handle empty states: Check if routines?.data is empty and display an appropriate message when no routines match the current page or filters.
Optimize images: The routine_imageUrl should be lazy-loaded and properly sized for better performance, especially with larger page sizes.
Both limit and page parameters are required. Omitting them will cause TypeScript errors. If you need default behavior, wrap the hook in a custom hook with defaults.

Common Patterns

Category Filtering

function CategoryFilter() {
  const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
  const { routines } = useRoutines(20, 1);

  const filtered = routines?.data.filter(routine => {
    if (!selectedCategory) return true;
    return routine.category.includes(selectedCategory);
  });

  // Get unique categories
  const categories = Array.from(
    new Set(routines?.data.flatMap(r => r.category) || [])
  );

  return (
    <div>
      <select onChange={(e) => setSelectedCategory(e.target.value || null)}>
        <option value="">All Categories</option>
        {categories.map(cat => (
          <option key={cat} value={cat}>{cat}</option>
        ))}
      </select>
      {/* Display filtered routines */}
    </div>
  );
}

Progressive Loading

function ProgressiveRoutines() {
  const [page, setPage] = useState(1);
  const [allRoutines, setAllRoutines] = useState<IRoutine[]>([]);
  const { routines, isLoading } = useRoutines(6, page);

  useEffect(() => {
    if (routines?.data) {
      setAllRoutines(prev => {
        const newRoutines = routines.data.filter(
          r => !prev.some(p => p.id === r.id)
        );
        return [...prev, ...newRoutines];
      });
    }
  }, [routines]);

  const hasMore = page < (routines?.totalPages || 0);

  return (
    <div>
      {allRoutines.map(routine => (
        <RoutineCard key={routine.id} routine={routine} />
      ))}
      {hasMore && (
        <button onClick={() => setPage(p => p + 1)} disabled={isLoading}>
          {isLoading ? 'Loading...' : 'Load More Routines'}
        </button>
      )}
    </div>
  );
}

Search and Filter

function RoutineSearch() {
  const [searchTerm, setSearchTerm] = useState('');
  const { routines } = useRoutines(50, 1); // Load more for client-side search

  const searchResults = routines?.data.filter(routine => {
    const title = routine.routine.routine_title.toLowerCase();
    const description = routine.routine.routine_description.toLowerCase();
    const term = searchTerm.toLowerCase();
    return title.includes(term) || description.includes(term);
  });

  return (
    <div>
      <input
        type="search"
        placeholder="Search routines..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      <p>{searchResults?.length || 0} routines found</p>
      {searchResults?.map(routine => (
        <RoutineCard key={routine.id} routine={routine} />
      ))}
    </div>
  );
}

Build docs developers (and LLMs) love