React Query Patterns
The Hook Pattern
Features expose a two-layer pattern:
- API function (
src/features/<feature>/api/) — Raw async function that calls axios - Hook (
src/features/<feature>/hooks/) — React Query hook wrapping the API function
Example: House Listings
API function (src/features/explore/api/list-houses.ts):
import { apiV2 } from '@/lib/axios';
import type { ListHousesParams, ListHousesResponse } from '../types';
export const listHouses = async (params: ListHousesParams): Promise<ListHousesResponse> => {
const queryParams: Record<string, any> = {};
if (params.limit) queryParams.limit = params.limit;
if (params.page) queryParams.page = params.page;
if (params.slug) queryParams.slug = params.slug;
// ... filter params
const response = await apiV2.get<ListHousesResponse>('properties/v1/houses', {
params: queryParams,
});
return response.data;
};Infinite query hook (src/features/explore/hooks/use-houses-query.ts) wraps listHouses with useInfiniteQuery.
Composite hook (src/features/explore/hooks/use-list-houses.ts) provides a higher-level API:
export const useListHouses = (options: UseListHousesOptions = {}): UseListHousesResult => {
const { slug, type, limit = 10, filters, sort } = options;
const queryClient = useQueryClient();
const { data, isLoading, isFetchingNextPage, isError, hasNextPage, fetchNextPage, refetch } =
useHousesQuery({ slug, type, limit, filters, sort });
// Deduplicates houses across pages
const houses = useMemo(() => {
if (!data?.pages) return [];
const allHouses = data.pages.flatMap((page) => page.data);
const seen = new Set<string>();
return allHouses.filter((house) => { /* dedupe */ });
}, [data?.pages]);
// Optimistic removal
const removeHouse = useCallback((houseId) => {
queryClient.setQueriesData({ queryKey: queryKeys.houses.all }, (oldData) => { /* filter */ });
}, [queryClient]);
return { houses, isLoading, isLoadingMore, hasError, pagination, hasMore, loadMore, refresh, removeHouse };
};Example: Wallet
API function (src/features/wallet/api/get-tokens.ts):
export const getWalletTokens = async (): Promise<WalletTokensResponse> => {
const response = await apiV2.get('perks/tokens');
return response.data;
};Query hook (src/features/wallet/hooks/use-wallet-query.ts):
export const useWalletQuery = (enabled: boolean = true) => {
return useQuery({
queryKey: walletQueryKeys.all,
queryFn: fetchWalletData, // combines tokens + event tickets
enabled,
staleTime: 5 * 60 * 1000,
});
};
export const useInvalidateWallet = () => {
const queryClient = useQueryClient();
return () => {
queryClient.invalidateQueries({ queryKey: walletQueryKeys.all });
};
};Query Keys
Each feature defines its query keys in a constants/query-keys.ts file:
// src/features/explore/constants/query-keys.ts
export const queryKeys = {
houses: {
all: ['houses'] as const,
list: (params: any) => ['houses', 'list', params] as const,
detail: (id: string | number) => ['houses', 'detail', id] as const,
},
};// src/features/wallet/constants/query-keys.ts
export const walletQueryKeys = {
all: ['wallet'] as const,
};Patterns in Use
Standard Query
Used for simple data fetching (events, perks, house detail):
useQuery({
queryKey: ['events', slug],
queryFn: () => getEvent(slug),
enabled: !!slug,
});Infinite Query
Used for paginated lists (house listings):
useInfiniteQuery({
queryKey: queryKeys.houses.list(params),
queryFn: ({ pageParam }) => listHouses({ ...params, page: pageParam }),
getNextPageParam: (lastPage) => lastPage.meta?.next_page,
initialPageParam: 1,
});Mutation with Cache Invalidation
const invalidateWallet = useInvalidateWallet();
useMutation({
mutationFn: (tokenId) => redeemToken(tokenId),
onSuccess: () => invalidateWallet(),
});Optimistic Updates
The removeHouse function in useListHouses demonstrates optimistic cache manipulation:
queryClient.setQueriesData({ queryKey: queryKeys.houses.all }, (oldData) => ({
...oldData,
pages: oldData.pages.map((page) => ({
...page,
data: page.data.filter((house) => String(house.id) !== String(houseId)),
})),
}));Non-Query Hooks
Some features use plain useState + useEffect hooks instead of React Query, typically for data that needs custom state management:
useHouse(src/features/explore/hooks/use-house.ts) — Fetches a single house withuseState, also records a viewuseViewings(src/features/viewings/hooks/use-viewings.ts) — Fetches viewings on screen focus withuseFocusEffect, client-side filtering/groupinguseRoomieProfiles(src/features/roomie/hooks/use-roomie-profiles.ts) — Manual pagination withuseState