This commit is contained in:
60
src/hooks/mutations/useAddWatchedItemMutation.ts
Normal file
60
src/hooks/mutations/useAddWatchedItemMutation.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// src/hooks/mutations/useAddWatchedItemMutation.ts
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import { notifySuccess, notifyError } from '../../services/notificationService';
|
||||
|
||||
interface AddWatchedItemParams {
|
||||
itemName: string;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutation hook for adding an item to the user's watched items list.
|
||||
*
|
||||
* This hook provides optimistic updates and automatic cache invalidation.
|
||||
* When the mutation succeeds, it invalidates the watched-items query to
|
||||
* trigger a refetch of the updated list.
|
||||
*
|
||||
* @returns Mutation object with mutate function and state
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const addWatchedItem = useAddWatchedItemMutation();
|
||||
*
|
||||
* const handleAdd = () => {
|
||||
* addWatchedItem.mutate(
|
||||
* { itemName: 'Milk', category: 'Dairy' },
|
||||
* {
|
||||
* onSuccess: () => console.log('Added!'),
|
||||
* onError: (error) => console.error(error),
|
||||
* }
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export const useAddWatchedItemMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ itemName, category }: AddWatchedItemParams) => {
|
||||
const response = await apiClient.addWatchedItem(itemName, category);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({
|
||||
message: `Request failed with status ${response.status}`,
|
||||
}));
|
||||
throw new Error(error.message || 'Failed to add watched item');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate and refetch watched items to get the updated list
|
||||
queryClient.invalidateQueries({ queryKey: ['watched-items'] });
|
||||
notifySuccess('Item added to watched list');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
notifyError(error.message || 'Failed to add item to watched list');
|
||||
},
|
||||
});
|
||||
};
|
||||
46
src/hooks/queries/useFlyerItemsQuery.ts
Normal file
46
src/hooks/queries/useFlyerItemsQuery.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// src/hooks/queries/useFlyerItemsQuery.ts
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import type { FlyerItem } from '../../types';
|
||||
|
||||
/**
|
||||
* Query hook for fetching items for a specific flyer.
|
||||
*
|
||||
* This hook is automatically disabled when no flyer ID is provided,
|
||||
* and caches data per-flyer to avoid refetching the same data.
|
||||
*
|
||||
* @param flyerId - The ID of the flyer to fetch items for
|
||||
* @returns Query result with flyer items data, loading state, and error state
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { data: flyerItems, isLoading, error } = useFlyerItemsQuery(flyer?.flyer_id);
|
||||
* ```
|
||||
*/
|
||||
export const useFlyerItemsQuery = (flyerId: number | undefined) => {
|
||||
return useQuery({
|
||||
queryKey: ['flyer-items', flyerId],
|
||||
queryFn: async (): Promise<FlyerItem[]> => {
|
||||
if (!flyerId) {
|
||||
throw new Error('Flyer ID is required');
|
||||
}
|
||||
|
||||
const response = await apiClient.fetchFlyerItems(flyerId);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({
|
||||
message: `Request failed with status ${response.status}`,
|
||||
}));
|
||||
throw new Error(error.message || 'Failed to fetch flyer items');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// API returns { items: FlyerItem[] }
|
||||
return data.items || [];
|
||||
},
|
||||
// Only run the query if we have a valid flyer ID
|
||||
enabled: !!flyerId,
|
||||
// Flyer items don't change, so cache them longer
|
||||
staleTime: 1000 * 60 * 5,
|
||||
});
|
||||
};
|
||||
39
src/hooks/queries/useFlyersQuery.ts
Normal file
39
src/hooks/queries/useFlyersQuery.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// src/hooks/queries/useFlyersQuery.ts
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import type { Flyer } from '../../types';
|
||||
|
||||
/**
|
||||
* Query hook for fetching flyers with pagination.
|
||||
*
|
||||
* This replaces the custom useInfiniteQuery hook with TanStack Query,
|
||||
* providing automatic caching, background refetching, and better state management.
|
||||
*
|
||||
* @param limit - Maximum number of flyers to fetch
|
||||
* @param offset - Number of flyers to skip
|
||||
* @returns Query result with flyers data, loading state, and error state
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { data: flyers, isLoading, error, refetch } = useFlyersQuery(20, 0);
|
||||
* ```
|
||||
*/
|
||||
export const useFlyersQuery = (limit: number = 20, offset: number = 0) => {
|
||||
return useQuery({
|
||||
queryKey: ['flyers', { limit, offset }],
|
||||
queryFn: async (): Promise<Flyer[]> => {
|
||||
const response = await apiClient.fetchFlyers(limit, offset);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({
|
||||
message: `Request failed with status ${response.status}`,
|
||||
}));
|
||||
throw new Error(error.message || 'Failed to fetch flyers');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
// Keep data fresh for 2 minutes since flyers don't change frequently
|
||||
staleTime: 1000 * 60 * 2,
|
||||
});
|
||||
};
|
||||
40
src/hooks/queries/useMasterItemsQuery.ts
Normal file
40
src/hooks/queries/useMasterItemsQuery.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// src/hooks/queries/useMasterItemsQuery.ts
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import type { MasterGroceryItem } from '../../types';
|
||||
|
||||
/**
|
||||
* Query hook for fetching all master grocery items.
|
||||
*
|
||||
* Master items are the canonical list of grocery items that users can watch
|
||||
* and that flyer items are mapped to. This data changes infrequently, so it's
|
||||
* cached with a longer stale time.
|
||||
*
|
||||
* @returns Query result with master items data, loading state, and error state
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { data: masterItems, isLoading, error } = useMasterItemsQuery();
|
||||
* ```
|
||||
*/
|
||||
export const useMasterItemsQuery = () => {
|
||||
return useQuery({
|
||||
queryKey: ['master-items'],
|
||||
queryFn: async (): Promise<MasterGroceryItem[]> => {
|
||||
const response = await apiClient.fetchMasterItems();
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({
|
||||
message: `Request failed with status ${response.status}`,
|
||||
}));
|
||||
throw new Error(error.message || 'Failed to fetch master items');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
// Master items change infrequently, keep data fresh for 10 minutes
|
||||
staleTime: 1000 * 60 * 10,
|
||||
// Cache for 30 minutes
|
||||
gcTime: 1000 * 60 * 30,
|
||||
});
|
||||
};
|
||||
39
src/hooks/queries/useShoppingListsQuery.ts
Normal file
39
src/hooks/queries/useShoppingListsQuery.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// src/hooks/queries/useShoppingListsQuery.ts
|
||||
import { useQuery } from '@tantml:parameter>
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import type { ShoppingList } from '../../types';
|
||||
|
||||
/**
|
||||
* Query hook for fetching the user's shopping lists.
|
||||
*
|
||||
* This hook is automatically disabled when the user is not authenticated,
|
||||
* and the cached data is invalidated when the user logs out.
|
||||
*
|
||||
* @param enabled - Whether the query should run (typically based on auth status)
|
||||
* @returns Query result with shopping lists data, loading state, and error state
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { data: shoppingLists, isLoading, error } = useShoppingListsQuery(!!user);
|
||||
* ```
|
||||
*/
|
||||
export const useShoppingListsQuery = (enabled: boolean) => {
|
||||
return useQuery({
|
||||
queryKey: ['shopping-lists'],
|
||||
queryFn: async (): Promise<ShoppingList[]> => {
|
||||
const response = await apiClient.fetchShoppingLists();
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({
|
||||
message: `Request failed with status ${response.status}`,
|
||||
}));
|
||||
throw new Error(error.message || 'Failed to fetch shopping lists');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
enabled,
|
||||
// Keep data fresh for 1 minute since users actively manage shopping lists
|
||||
staleTime: 1000 * 60,
|
||||
});
|
||||
};
|
||||
39
src/hooks/queries/useWatchedItemsQuery.ts
Normal file
39
src/hooks/queries/useWatchedItemsQuery.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// src/hooks/queries/useWatchedItemsQuery.ts
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import type { MasterGroceryItem } from '../../types';
|
||||
|
||||
/**
|
||||
* Query hook for fetching the user's watched items.
|
||||
*
|
||||
* This hook is automatically disabled when the user is not authenticated,
|
||||
* and the cached data is invalidated when the user logs out.
|
||||
*
|
||||
* @param enabled - Whether the query should run (typically based on auth status)
|
||||
* @returns Query result with watched items data, loading state, and error state
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { data: watchedItems, isLoading, error } = useWatchedItemsQuery(!!user);
|
||||
* ```
|
||||
*/
|
||||
export const useWatchedItemsQuery = (enabled: boolean) => {
|
||||
return useQuery({
|
||||
queryKey: ['watched-items'],
|
||||
queryFn: async (): Promise<MasterGroceryItem[]> => {
|
||||
const response = await apiClient.fetchWatchedItems();
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({
|
||||
message: `Request failed with status ${response.status}`,
|
||||
}));
|
||||
throw new Error(error.message || 'Failed to fetch watched items');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
enabled,
|
||||
// Keep data fresh for 1 minute since users actively manage watched items
|
||||
staleTime: 1000 * 60,
|
||||
});
|
||||
};
|
||||
@@ -1,28 +1,31 @@
|
||||
// src/hooks/useFlyerItems.ts
|
||||
import type { Flyer, FlyerItem } from '../types';
|
||||
import { useApiOnMount } from './useApiOnMount';
|
||||
import * as apiClient from '../services/apiClient';
|
||||
import type { Flyer } from '../types';
|
||||
import { useFlyerItemsQuery } from './queries/useFlyerItemsQuery';
|
||||
|
||||
/**
|
||||
* A custom hook to fetch the items for a given flyer.
|
||||
* A custom hook to fetch the items for a given flyer using TanStack Query (ADR-0005).
|
||||
*
|
||||
* This replaces the previous useApiOnMount implementation with TanStack Query
|
||||
* for automatic caching and better state management.
|
||||
*
|
||||
* @param selectedFlyer The flyer for which to fetch items.
|
||||
* @returns An object containing the flyer items, loading state, and any errors.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { flyerItems, isLoading, error } = useFlyerItems(selectedFlyer);
|
||||
* ```
|
||||
*/
|
||||
export const useFlyerItems = (selectedFlyer: Flyer | null) => {
|
||||
const wrappedFetcher = (flyerId?: number): Promise<Response> => {
|
||||
// This should not be called with undefined due to the `enabled` flag,
|
||||
// but this wrapper satisfies the type checker.
|
||||
if (flyerId === undefined) {
|
||||
return Promise.reject(new Error('Cannot fetch items for an undefined flyer ID.'));
|
||||
}
|
||||
return apiClient.fetchFlyerItems(flyerId);
|
||||
};
|
||||
const {
|
||||
data: flyerItems = [],
|
||||
isLoading,
|
||||
error,
|
||||
} = useFlyerItemsQuery(selectedFlyer?.flyer_id);
|
||||
|
||||
const { data, loading, error } = useApiOnMount<{ items: FlyerItem[] }, [number?]>(
|
||||
wrappedFetcher,
|
||||
[selectedFlyer],
|
||||
{ enabled: !!selectedFlyer },
|
||||
selectedFlyer?.flyer_id,
|
||||
);
|
||||
return { flyerItems: data?.items || [], isLoading: loading, error };
|
||||
return {
|
||||
flyerItems,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user