State Management¶
Carbon Connect uses React Query (TanStack Query) for server state management and custom hooks for UI state.
React Query¶
React Query manages all server state: data fetching, caching, background updates, and optimistic mutations.
Provider Setup¶
The providers.tsx file at the app root configures React Query:
// src/app/providers.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 30 * 60 * 1000, // 30 minutes
retry: 2,
refetchOnWindowFocus: false,
},
},
});
API Hooks¶
All React Query hooks are organized by domain in src/lib/api/hooks/:
Hook Catalog¶
| Module | Hooks | Domain |
|---|---|---|
useAuth | useCurrentUser, useLogin, useRegister, useLogout, useUpdateProfile, useChangePassword, useIsAuthenticated | Authentication |
useCompanies | useCompanies, useCompany, useCreateCompany, useUpdateCompany, useDeleteCompany, useUploadCompanyLogo | Company management |
useGrants | useGrants, useGrant, useGrantSearch, useInfiniteGrants, useInfiniteGrantSearch, useSimilarGrants, useGrantSources, useUpcomingDeadlines, useGrantStats, useCarbonGrants | Grant search |
useMatches | useMatches, useMatch, useSavedMatches, useMatchStats, useCalculateMatches, useSaveMatch, useDismissMatch, useRestoreMatch | Grant matching |
useApplications | useApplications, useApplication, useApplicationsByStatus, useCreateApplication, useUpdateApplication, useDeleteApplication, useGenerateContent, useUploadDocument | Applications |
useDashboard | useDashboardStats, useRecentActivity | Dashboard |
useReference | useNaceCodes, useCountries, useEuCountries | Reference data |
useCarbonProfile | useCarbonProfile, useUpdateCarbonProfile, useCalculateEmissions, useResetCarbonProfile, useCarbonRecommendedGrants | Carbon profile |
useVcri | useVcriProjects, useVcriProject, useCreateVcriProject, useSetVcriBaseline, useCreateVcriMeasurement, useIssueVcriCertificate | VCRI projects |
Query Keys¶
Each module defines a queryKeys factory for cache management:
export const grantKeys = {
all: ["grants"] as const,
lists: () => [...grantKeys.all, "list"] as const,
list: (params: GrantSearchParams) => [...grantKeys.lists(), params] as const,
details: () => [...grantKeys.all, "detail"] as const,
detail: (id: string) => [...grantKeys.details(), id] as const,
search: (params: FullTextSearchParams) => [...grantKeys.all, "search", params] as const,
stats: () => [...grantKeys.all, "stats"] as const,
sources: () => [...grantKeys.all, "sources"] as const,
};
Query Hook Example¶
export function useGrants(params: GrantSearchParams = {}) {
return useQuery({
queryKey: grantKeys.list(params),
queryFn: () => grantApi.search(params),
staleTime: 5 * 60 * 1000,
});
}
Mutation Hook Example¶
export function useCreateCompany() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CompanyCreateRequest) => companyApi.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: companyKeys.all });
},
});
}
Infinite Scroll¶
For paginated data with infinite scrolling:
export function useInfiniteGrants(params: GrantSearchParams = {}) {
return useInfiniteQuery({
queryKey: grantKeys.list({ ...params, infinite: true }),
queryFn: ({ pageParam = 1 }) =>
grantApi.search({ ...params, page: pageParam }),
getNextPageParam: (lastPage) =>
lastPage.page < lastPage.total_pages ? lastPage.page + 1 : undefined,
initialPageParam: 1,
});
}
API Client¶
Source: src/lib/api/client.ts
The API client provides a type-safe fetch wrapper with automatic token management:
Features¶
- Automatic JWT token refresh on 401 responses
- Token queue for concurrent 401 handling (prevents multiple refresh calls)
- Type-safe methods --
get<T>(),post<T>(),patch<T>(),delete<T>() - Query parameter building from typed objects
- Error normalization via
ApiClientErrorclass
Token Management¶
// Tokens stored in localStorage
getStoredTokens(): { accessToken, refreshToken }
storeTokens(tokens: AuthTokens): void
clearTokens(): void
Request Flow¶
sequenceDiagram
participant C as Component
participant H as React Query Hook
participant A as API Client
participant S as Backend API
C->>H: useGrants({ country: "DE" })
H->>A: apiClient.get("/api/v1/grants", params)
A->>A: Add Bearer token from localStorage
A->>S: GET /api/v1/grants?country=DE
S-->>A: 200 OK + JSON
A-->>H: Typed response
H-->>C: { data, isLoading, error }
Note over A,S: On 401 response:
A->>S: POST /api/v1/auth/refresh
S-->>A: New tokens
A->>A: Store new tokens
A->>S: Retry original request Custom Hooks¶
Source: src/hooks/
Non-API hooks for UI behavior:
| Hook | Purpose |
|---|---|
useToast | Toast notification management |
useKeyboardShortcuts | Register keyboard shortcuts |
useGlobalShortcuts | Application-wide shortcuts (Cmd+K, etc.) |
useReducedMotion | Respect user's reduced motion preference |
useMotionDuration | Adaptive animation duration |
useAnimatedCounter | Animated number transitions |
useScrollAnimation | Intersection observer-based animations |
useScrollProgress | Scroll position tracking |
Usage Example¶
import { useToast, useKeyboardShortcuts } from "@/hooks";
export function GrantDetail() {
const { toast } = useToast();
useKeyboardShortcuts([
{ key: "s", ctrl: true, handler: () => handleSave() },
{ key: "Escape", handler: () => handleClose() },
]);
const handleSave = () => {
toast({ title: "Grant saved", type: "success" });
};
}
Endpoint Definitions¶
Source: src/lib/api/endpoints.ts
All API calls are defined in domain-specific objects:
| Object | Endpoint Prefix | Methods |
|---|---|---|
authApi | /api/v1/auth | login, register, logout, getCurrentUser |
companyApi | /api/v1/companies | list, get, create, update, delete |
grantApi | /api/v1/grants | search, fullTextSearch, get, getSimilar, getStats |
matchApi | /api/v1/matches | getForCompany, get, calculate, updateAction |
applicationApi | /api/v1/applications | list, get, create, update, delete, generateContent |
dashboardApi | /api/v1/dashboard | getStats, getRecentActivity |
referenceApi | /api/v1/reference | searchNaceCodes, getCountries |
carbonProfileApi | /api/v1/companies/{id}/carbon-profile | get, update, calculate, reset |
vcriApi | /api/v1/vcri | listProjects, createProject, setBaseline, createMeasurement |