The skill is for consistently using useSuspenseQuery from TanStack Query (React Query) in a standardized way across all frontend applications. Here's the pattern observed:
Basic Usage Pattern
const { data } = useSuspenseQuery({
queryKey: ["query-key", parameter1, parameter2],
queryFn: () => fetchFunction(parameter1, parameter2),
});
Common Examples from the Codebase
- •Simple data fetching:
const { data: bids } = useSuspenseQuery({
queryKey: ["bids", filters, type],
queryFn: () => fetchBids(filters, type),
});
- •Specific data:
const { data } = useSuspenseQuery({
queryKey: ["key", id],
queryFn: () => fetchData(id),
});
Key Characteristics Observed
- •Consistent naming: The hook result is destructured with data as the variable name (e.g., const { data: w9Data } = useSuspenseQuery(...))
- •Query key patterns:
- •Simple strings for static queries: ["vendors"]
- •Parameterized keys for dynamic queries: ["vendor", vendorId]
- •Complex keys with multiple parameters: ["bids", filters, type]
- •Integration with React Query utilities:
- •Often used alongside getQueryClient for cache invalidation on both server and client-side
- •Combined with useMutation for CRUD operations
- •Used in conjunction with error handling and loading states
- •Error handling:
- •Some components check for errors explicitly (like in flh-traffic-module-frontend)
- •Often combined with toast notifications for user feedback
- •Testing patterns:
- •Mocked in test files using vi.fn() to simulate data fetching
- •Tests typically mock the query key and return expected data structures
Common Use Cases
- •Loading data on page load: Used in component initialization to fetch required data
- •Content-specific information: Fetching data for specific records.
- •Table/list data: Loading data for dynamic grids and tables
Best Practices Observed
Looking at the existing codebase, you should also ensure that:
- •Query keys are consistently structured with a predictable pattern that includes the resource type and identifiers
- •Error handling is implemented where appropriate, particularly in traffic/FLH modules
- •Integration with mutations is common - query invalidation happens after mutations to keep data fresh
- •The query is properly prefetched on the server-side in the page component (as
shown in
page.tsxfiles):
void queryClient.prefetchQuery({
queryKey: ["resource", id],
queryFn: () => fetchResource(id),
});
- •Use the same query key pattern that's already established in the codebase:
- •["resource", id] for resource data
- •This maintains consistency with how other queries are handled
Key Points to Remember
- •Always use proper import: Import getQueryClient() from "~/app/get-query-client" over useSuspenseQuery from "@tanstack/react-query".
- •Use consistent query keys: Follow the pattern ["resource", id]
- •Error handling: The suspense query will automatically handle loading states and errors
- •Type safety: Make sure your action function returns proper data types that match your expectations
- •Integration with existing code: The modules already have a pattern of using this approach, so maintain consistency
useMutation Pattern
This project uses a standardized pattern for data mutations (create, update, delete operations) with TanStack Query. The pattern ensures consistent error handling, loading states, and cache management.
Basic Mutation Structure
const { mutate: actionMutate, isPending: isActionPending } = useMutation({
mutationFn: (params) => actionFunction(params),
onSuccess: async () => {
// Success handling
toast.success("Success message");
// Refresh related queries
await queryClient.invalidateQueries({
queryKey: ["query-key", parameter],
});
// Update UI state and navigate
setLocalState(updatedValue);
closeModal();
router.push(`/path/${parameter}`);
},
onError: (error) => {
// Error handling
console.error("Error:", error);
toast.error(
error instanceof Error ? error.message : "Generic error message",
);
router.refresh();
},
onSettled: () => {
// Cleanup
form.reset();
},
});
Key Principles
- •
Consistent Naming:
- •Mutation functions follow
actionMutatepattern - •Loading states use descriptive names like
isCreating,isUpdating - •Destructure from useMutation for clarity
- •Mutation functions follow
- •
Error Handling:
- •Always check
error instanceof Errorbefore accessing properties - •Provide generic fallback messages
- •Log errors and show user-friendly notifications
- •Always check
- •
Success Flow:
- •Toast notifications for immediate feedback
- •Query invalidation to refresh data
- •Local state updates as needed
- •Navigation or modal management
- •
Loading States:
- •Individual states per mutation
- •Combine states for complex operations
- •Disable UI elements during mutations
Common Patterns
Form Integration
<form
onSubmit={(e) => {
e.preventDefault();
mutationMutate({ param1: value1, param2: value2 });
}}
>
{/* Form fields */}
<SubmitButton disabled={isPending}>Submit</SubmitButton>
</form>
Query Invalidation
// Single query
await queryClient.invalidateQueries({
queryKey: ["resource", id],
});
// Multiple related queries
await queryClient.invalidateQueries({ queryKey: ["resource", id] });
await queryClient.invalidateQueries({ queryKey: ["related-data", id] });
Combined Loading States
const isProcessing = isCreating || isUpdating || isDeleting;
Best Practices
- •Type Safety: Use TypeScript interfaces for mutation parameters
- •Server Actions: Use "use server" directive for mutation functions
- •Consistent Messaging: Follow predictable error message patterns
- •Loading Feedback: Disable buttons and show indicators during mutations
- •Form Cleanup: Use
onSettledfor form resets and cleanup - •Navigation: Handle redirects appropriately after mutations
Custom Hooks (Optional)
For reusable mutations, extract to custom hooks:
export const useCreateResource = (onSuccess?: () => void) =>
useMutation({
mutationFn: createResource,
onSuccess: (data) => {
toast.success("Successfully created resource.");
onSuccess?.();
},
onError: (error: Error) => {
toast.error(error.message || "Failed to create resource");
},
});
Key Characteristics
- •
Consistent Naming:
- •Mutation function:
actionMutate(e.g.,createContactMutate,updateContactMutate) - •Loading state:
isActionPending(e.g.,isCreating,isUpdating) - •Destructured from useMutation for clarity
- •Mutation function:
- •
Error Handling:
- •Always check
error instanceof Errorbefore accessingerror.message - •Provide generic fallback messages
- •Log errors to console for debugging
- •Show user-friendly toast notifications
- •Always check
- •
Success Flow:
- •Toast notifications for immediate feedback
- •Query invalidation to refresh related data
- •Local state updates when needed
- •Navigation or modal closing
- •
Loading States:
- •Individual loading states per mutation
- •Combined loading states for complex operations:
const isPending = isCreating || isUpdating || isRemoving; - •UI elements disabled during mutations
Common Examples from Codebase
Create Mutation (VendorContacts.tsx)
const { mutate: createContactMutate, isPending: isCreating } = useMutation({
mutationFn: (data: ContactFormValues) =>
addVendorContact(vendorId, {
name: data.name,
role: data.role,
phone: data.phone || "",
cellNumber: data.cellNumber || "",
email: data.email,
isPrimary: data.isPrimary ?? false,
}),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: ["vendor-contacts", vendorId],
});
const primaryContactCount = await countPrimaryContacts(vendorId);
setHasPrimaryVendorContact(primaryContactCount === 1);
setFormModalState(false);
toast.success("Successfully added vendor contact!");
router.push(`/vendors/${vendorId}`);
},
onError: (error) => {
console.error("Error updating contact:", error);
toast.error(
error instanceof Error ? error.message : "Error saving contact!",
);
router.refresh();
},
onSettled: () => form.reset(),
});
Delete Mutation Pattern
const { mutate: removeContactMutate, isPending: isRemoving } = useMutation({
mutationFn: async (contactId: string) => {
await removeVendorContact(contactId);
return contactId;
},
onSuccess: async (contactId: string) => {
toast.success("Successfully removed vendor contact!");
await queryClient.invalidateQueries({
queryKey: ["vendor-contacts", vendorId],
});
const primaryContactCount = await countPrimaryContacts(vendorId);
setHasPrimaryVendorContact(primaryContactCount === 1);
router.push(`/vendors/${vendorId}`);
},
onError: (error) => {
console.error("Error removing contact:", error);
toast.error(
error instanceof Error ? error.message : "Failed to remove contact",
);
router.refresh();
},
onSettled: () => setRemoveModalState(false),
});
Form Integration Pattern
<form
onSubmit={(e) => {
e.preventDefault();
mutationMutate({ param1: value1, param2: value2 });
}}
>
<Field>
<FieldContent>
<Textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
required
placeholder="Enter notes..."
/>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Close</Button>
</DialogClose>
<SubmitButton variant="destructive" disabled={isPending}>
Submit Action
</SubmitButton>
</DialogFooter>
</FieldContent>
</Field>
</form>
Query Invalidation Patterns
- •
Targeted Invalidation: Always use specific query keys
javascriptawait queryClient.invalidateQueries({ queryKey: ["vendor-documents", vendorId], }); - •
Multiple Related Queries: When mutations affect multiple data types
javascriptawait queryClient.invalidateQueries({ queryKey: ["vendor-documents", vendorId], }); await queryClient.invalidateQueries({ queryKey: ["audit-logs", vendorId] }); await queryClient.invalidateQueries({ queryKey: ["vendor-document-counts", vendorId], }); - •
Sequential Operations: Perform invalidations in order, sometimes with additional async operations
Best Practices
- •Type Safety: Define proper TypeScript interfaces for mutation parameters
- •Server Actions: All mutation functions should be server actions with "use server" directive
- •Consistent Error Messages: Use predictable error message patterns
- •Loading State Management: Disable buttons and show loading indicators during mutations
- •Form Reset: Use
onSettledcallback to clean up form state - •Navigation: Use
router.push()orrouter.refresh()appropriately after mutations
Custom Hook Pattern (Advanced)
For reusable mutations, create custom hooks:
export const useCreateBid = (onSuccess?: () => void) =>
useMutation({
mutationFn: createBid,
onSuccess: (data) => {
toast.success("Successfully created bid.");
onSuccess?.();
},
onError: (error: Error) => {
toast.error(error.message || "Failed to create bid", {
duration: Infinity,
action: {
label: "Dismiss",
onClick: () => console.log("Error was dismissed"),
},
});
},
});
Summary
The implementation in the modules follows a consistent pattern where:
- •Server-side prefetching is used to pre-load queries
- •Client-side getQueryClient or useSuspenseQuery is used for components that need to fetch data
- •useMutation follows standardized patterns for all CRUD operations
- •Query keys follow a predictable naming convention
- •The same fetch action function is used consistently
This approach ensures proper hydration, loading states, consistent caching behavior, and maintainable mutation logic across the application.