Some checks failed
Continuous Integration / Frontend Tests (React + Vitest) (push) Has been cancelled
Continuous Integration / Backend Tests (Python + pytest) (push) Has been cancelled
Continuous Integration / Docker Build Tests (agents) (push) Has been cancelled
Continuous Integration / Docker Build Tests (frontend) (push) Has been cancelled
Continuous Integration / Docker Build Tests (mcp) (push) Has been cancelled
Continuous Integration / Docker Build Tests (server) (push) Has been cancelled
Continuous Integration / Test Results Summary (push) Has been cancelled
* refactor: reorganize features/shared directory structure - Created organized subdirectories for better code organization: - api/ - API clients and HTTP utilities (renamed apiWithEtag.ts to apiClient.ts) - config/ - Configuration files (queryClient, queryPatterns) - types/ - Shared type definitions (errors) - utils/ - Pure utility functions (optimistic, clipboard) - hooks/ - Shared React hooks (already existed) - Updated all import paths across the codebase (~40+ files) - Updated all AI documentation in PRPs/ai_docs/ to reflect new structure - All tests passing, build successful, no functional changes This improves maintainability and follows vertical slice architecture patterns. Co-Authored-By: Claude <noreply@anthropic.com> * fix: address PR review comments and code improvements - Update imports to use @/features alias path for optimistic utils - Fix optimistic upload item replacement by matching on source_id instead of id - Clean up test suite naming and remove meta-terms from comments - Only set Content-Type header on requests with body - Add explicit TypeScript typing to useProjectFeatures hook - Complete Phase 4 improvements with proper query typing * fix: address additional PR review feedback - Clear feature queries when deleting project to prevent cache memory leaks - Update KnowledgeCard comments to follow documentation guidelines - Add explanatory comment for accessibility pattern in KnowledgeCard --------- Co-authored-by: Claude <noreply@anthropic.com>
7.1 KiB
7.1 KiB
TanStack Query Patterns Guide
This guide documents the standardized patterns for using TanStack Query v5 in the Archon frontend.
Core Principles
- Feature Ownership: Each feature owns its query keys in
{feature}/hooks/use{Feature}Queries.ts - Consistent Patterns: Always use shared patterns from
shared/config/queryPatterns.ts - No Hardcoded Values: Never hardcode stale times or disabled keys
- Mirror Backend API: Query keys should exactly match backend API structure
Query Key Factory Pattern
Every feature MUST implement a query key factory following this pattern:
// features/{feature}/hooks/use{Feature}Queries.ts
export const featureKeys = {
all: ["feature"] as const, // Base key for the domain
lists: () => [...featureKeys.all, "list"] as const, // For list endpoints
detail: (id: string) => [...featureKeys.all, "detail", id] as const, // For single item
// Add more as needed following backend routes
};
Examples from Codebase
// Projects - Simple hierarchy
export const projectKeys = {
all: ["projects"] as const,
lists: () => [...projectKeys.all, "list"] as const,
detail: (id: string) => [...projectKeys.all, "detail", id] as const,
features: (id: string) => [...projectKeys.all, id, "features"] as const,
};
// Tasks - Dual nature (global and project-scoped)
export const taskKeys = {
all: ["tasks"] as const,
lists: () => [...taskKeys.all, "list"] as const, // /api/tasks
detail: (id: string) => [...taskKeys.all, "detail", id] as const,
byProject: (projectId: string) => ["projects", projectId, "tasks"] as const, // /api/projects/{id}/tasks
counts: () => [...taskKeys.all, "counts"] as const,
};
Shared Patterns Usage
Import Required Patterns
import { DISABLED_QUERY_KEY, STALE_TIMES } from "@/features/shared/config/queryPatterns";
Disabled Queries
Always use DISABLED_QUERY_KEY when a query should not execute:
// ✅ CORRECT
queryKey: projectId ? projectKeys.detail(projectId) : DISABLED_QUERY_KEY,
// ❌ WRONG - Don't create custom disabled keys
queryKey: projectId ? projectKeys.detail(projectId) : ["projects-undefined"],
Stale Times
Always use STALE_TIMES constants for cache configuration:
// ✅ CORRECT
staleTime: STALE_TIMES.normal, // 30 seconds
staleTime: STALE_TIMES.frequent, // 5 seconds
staleTime: STALE_TIMES.instant, // 0 - always fresh
// ❌ WRONG - Don't hardcode times
staleTime: 30000,
staleTime: 0,
STALE_TIMES Reference
instant: 0- Always fresh (real-time data like active progress)realtime: 3_000- 3 seconds (near real-time updates)frequent: 5_000- 5 seconds (frequently changing data)normal: 30_000- 30 seconds (standard cache time)rare: 300_000- 5 minutes (rarely changing config)static: Infinity- Never stale (settings, auth)
Complete Hook Pattern
export function useFeatureDetail(id: string | undefined) {
return useQuery({
queryKey: id ? featureKeys.detail(id) : DISABLED_QUERY_KEY,
queryFn: () => id
? featureService.getFeatureById(id)
: Promise.reject("No ID provided"),
enabled: !!id,
staleTime: STALE_TIMES.normal,
});
}
Mutations with Optimistic Updates
import { createOptimisticEntity, replaceOptimisticEntity } from "@/features/shared/utils/optimistic";
export function useCreateFeature() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateFeatureRequest) => featureService.create(data),
onMutate: async (newData) => {
// Cancel in-flight queries
await queryClient.cancelQueries({ queryKey: featureKeys.lists() });
// Snapshot for rollback
const previous = queryClient.getQueryData(featureKeys.lists());
// Optimistic update with nanoid for stable IDs
const optimisticEntity = createOptimisticEntity(newData);
queryClient.setQueryData(featureKeys.lists(), (old: Feature[] = []) =>
[...old, optimisticEntity]
);
return { previous, localId: optimisticEntity._localId };
},
onError: (err, variables, context) => {
// Rollback on error
if (context?.previous) {
queryClient.setQueryData(featureKeys.lists(), context.previous);
}
},
onSuccess: (data, variables, context) => {
// Replace optimistic with real data
queryClient.setQueryData(featureKeys.lists(), (old: Feature[] = []) =>
replaceOptimisticEntity(old, context?.localId, data)
);
},
});
}
Testing Query Hooks
Always mock both services and shared patterns:
// Mock services
vi.mock("../../services", () => ({
featureService: {
getList: vi.fn(),
getById: vi.fn(),
},
}));
// Mock shared patterns with ALL values
vi.mock("../../../shared/config/queryPatterns", () => ({
DISABLED_QUERY_KEY: ["disabled"] as const,
STALE_TIMES: {
instant: 0,
realtime: 3_000,
frequent: 5_000,
normal: 30_000,
rare: 300_000,
static: Infinity,
},
}));
Vertical Slice Architecture
Each feature is self-contained:
src/features/projects/
├── components/ # UI components
├── hooks/
│ └── useProjectQueries.ts # Query hooks & keys
├── services/
│ └── projectService.ts # API calls
└── types/
└── index.ts # TypeScript types
Sub-features (like tasks under projects) follow the same structure:
src/features/projects/tasks/
├── components/
├── hooks/
│ └── useTaskQueries.ts # Own query keys!
├── services/
└── types/
Migration Checklist
When refactoring to these patterns:
- Create query key factory in
hooks/use{Feature}Queries.ts - Import
DISABLED_QUERY_KEYandSTALE_TIMESfrom shared - Replace all hardcoded disabled keys with
DISABLED_QUERY_KEY - Replace all hardcoded stale times with
STALE_TIMESconstants - Update all
queryKeyreferences to use factory - Update all
invalidateQueriesto use factory - Update all
setQueryDatato use factory - Add comprehensive tests for query keys
- Remove any backward compatibility code
Common Pitfalls to Avoid
- Don't create centralized query keys - Each feature owns its keys
- Don't hardcode values - Use shared constants
- Don't mix concerns - Tasks shouldn't import projectKeys
- Don't skip mocking in tests - Mock both services and patterns
- Don't use inconsistent patterns - Follow the established conventions
Completed Improvements (Phases 1-5)
- ✅ Phase 1: Removed manual frontend ETag cache layer (backend ETags remain; browser-managed)
- ✅ Phase 2: Standardized query keys with factories
- ✅ Phase 3: Implemented UUID-based optimistic updates using nanoid
- ✅ Phase 4: Configured request deduplication
- ✅ Phase 5: Removed manual cache invalidations
Future Considerations
- Add Server-Sent Events for real-time updates
- Consider WebSocket fallback for critical updates
- Evaluate Zustand for complex client state management