Background
The panel components all fetch through apiClient but each unwraps the response differently and handles the lifecycle inconsistently. src/components/ai/SmartNotifications.tsx reads res.data, src/components/ai/PersonalizedRecommendations.tsx reads r.items, src/components/ai/NaturalLanguageQuery.tsx reads r.results, and src/components/ai/LearningAssistant.tsx reads r.reply, while src/components/ai/IntelligentProgress.tsx and src/components/social/FollowingSystem.tsx read the payload directly. Only SmartNotifications guards against setState-after-unmount; the others can update unmounted components, and none pass an AbortSignal even though src/lib/api.ts (RequestConfig extends RequestInit) and src/hooks/useAbortController.ts already support cancellation. src/types/api.ts defines ApiResponse but it is applied inconsistently, so each panel reinvents its own loading/error state.
Implementation Plan
- Add a shared hook src/hooks/useApiResource.ts that wraps apiClient, threads an AbortController (leveraging src/hooks/useAbortController.ts), exposes { data, loading, error, refetch }, and unwraps the canonical ApiResponse envelope consistently.
- Ensure src/lib/api.ts forwards a caller-supplied signal (composed with its internal timeout controller) and document the canonical response envelope in src/types/api.ts.
- Adopt the hook in SmartNotifications.tsx, PersonalizedRecommendations.tsx, NaturalLanguageQuery.tsx, LearningAssistant.tsx, IntelligentProgress.tsx, social/FollowingSystem.tsx, and ExportButton.tsx, removing the bespoke useEffect/fetch logic and per-file loading/error state.
- Add benchmarks/metrics capturing before/after (e.g. dropped-request counts and unmount-safety).
- Add regression tests.
- Document the change.
Files in Scope (10)
src/hooks/useApiResource.ts (new), src/lib/api.ts, src/types/api.ts, src/components/ai/SmartNotifications.tsx, src/components/ai/PersonalizedRecommendations.tsx, src/components/ai/NaturalLanguageQuery.tsx, src/components/ai/LearningAssistant.tsx, src/components/ai/IntelligentProgress.tsx, src/components/social/FollowingSystem.tsx, src/components/ExportButton.tsx
Acceptance Criteria
- Measurable performance/efficiency improvement demonstrated with before/after numbers
- No regression in existing functionality
- Tests pass and code follows project standards
- Change is documented
Difficulty
Medium-Hard
Background
The panel components all fetch through apiClient but each unwraps the response differently and handles the lifecycle inconsistently. src/components/ai/SmartNotifications.tsx reads res.data, src/components/ai/PersonalizedRecommendations.tsx reads r.items, src/components/ai/NaturalLanguageQuery.tsx reads r.results, and src/components/ai/LearningAssistant.tsx reads r.reply, while src/components/ai/IntelligentProgress.tsx and src/components/social/FollowingSystem.tsx read the payload directly. Only SmartNotifications guards against setState-after-unmount; the others can update unmounted components, and none pass an AbortSignal even though src/lib/api.ts (RequestConfig extends RequestInit) and src/hooks/useAbortController.ts already support cancellation. src/types/api.ts defines ApiResponse but it is applied inconsistently, so each panel reinvents its own loading/error state.
Implementation Plan
Files in Scope (10)
src/hooks/useApiResource.ts (new), src/lib/api.ts, src/types/api.ts, src/components/ai/SmartNotifications.tsx, src/components/ai/PersonalizedRecommendations.tsx, src/components/ai/NaturalLanguageQuery.tsx, src/components/ai/LearningAssistant.tsx, src/components/ai/IntelligentProgress.tsx, src/components/social/FollowingSystem.tsx, src/components/ExportButton.tsx
Acceptance Criteria
Difficulty
Medium-Hard