Skip to Content
Housr AppReact Query Patterns

React Query Patterns

The Hook Pattern

Features expose a two-layer pattern:

  1. API function (src/features/<feature>/api/) — Raw async function that calls axios
  2. 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 with useState, also records a view
  • useViewings (src/features/viewings/hooks/use-viewings.ts) — Fetches viewings on screen focus with useFocusEffect, client-side filtering/grouping
  • useRoomieProfiles (src/features/roomie/hooks/use-roomie-profiles.ts) — Manual pagination with useState
Last updated on