SWR Data Fetching Standardization

Date
ImpactCritical

Background

AlgoSu's frontend was manually managing all data fetching with useState + useEffect + useCallback boilerplate. The same 3-state pattern (data/isLoading/error) was repeated across all pages — problems, submissions, settings, dashboard — with high maintenance cost and lacking basic features like caching, revalidation, and duplicate request prevention.

Selected as the top priority among MEMORY.md "ongoing follow-up" section's 3 items (SWR introduction, Redis statistics cache, problem.tags JSON migration) and organized as a single sprint.

Goals

ItemContentStatus
Phase ASWR infrastructure setup (fetcher, Provider, 4 hooks, test-utils)✅ Complete
Phase B3 page migrations (problems/submissions/settings)✅ Complete
Phase CNotificationBell polling → refreshInterval migration✅ Complete
Phase DNew hook tests + existing page test updates✅ Complete

Decisions

D1. SWR Selection (vs TanStack Query / React Query)

Background: Frontend data fetching library selection. Comparison of two candidates needed.

Options:

  • (A) TanStack Query (React Query) — advanced caching/sync, bundle ~40KB, rich DevTools
  • (B) SWR — lightweight ~8KB, simple API, httpOnly Cookie friendly ← selected

Selected: (B) — Precisely matches AlgoSu's tech stack:

  • httpOnly Cookie auth: automatic credentials: 'include' handling, no manual Authorization header injection
  • Minimal change integration with existing fetch wrapper (fetchApi) — use API path as key directly
  • No conflicts with SSE + polling real-time features
  • Bundle size advantage (~8KB)
  • Current data fetching complexity (simple CRUD) doesn't require React Query's advanced features

Result: [email protected] introduced. Common fetcher + cacheKeys factory in lib/swr.ts, global config via SWRProvider.


D2. Cache Key = API path (No Separate Key Scheme)

Background: SWR indexes cache by key — key naming convention needed.

Options:

  • (A) Domain-based tuple keys (e.g., ['problems', 'all', studyId])
  • (B) API path directly (e.g., /api/problems/all) ← selected

Selected: (B) — fetchApi(path) signature is already path-based, so key = path makes fetcher ((key) => fetchApi(key)) naturally valid. 1:1 mapping with network tab for cache key debugging. Type safety ensured only via cacheKeys factory.

Result: cacheKeys.problems.all() → returns /api/problems/all. Query parameters serialized to path via URLSearchParams.


D3. invalidateAllCache() Call on Study Switch

Background: fetchApi internally reads module-level _currentStudyId to inject X-Study-ID header. Even with the same path, server responses differ when study changes — if studyId is absent from SWR cache key, previous study data appears stale.

Options:

  • (A) Include studyId in all study-scoped keys (e.g., ['study-1', '/api/problems/all'])
  • (B) Invalidate entire SWR cache on study switch event ← selected

Selected: (B) — High key management cost for (A) due to many scoped data items. Study switching is an infrequent event — full revalidation overhead is acceptable. invalidateAllCache() called in StudyContext.setCurrentStudy.

Result: invalidateAllCache() exported from lib/swr.ts. Called in StudyContext.tsx on studyId change → all SWR caches revalidated.


D4. SWR Exclusions: SSE hooks / useAutoSave / Search hooks

Background: 7 custom hooks in frontend. Need to decide scope for SWR migration.

Selected: 4 to migrate + 3 to exclude:

  • Migrate: useProblems, useStudyStats, useSubmissions, useProfileSettings (simple GET read cache)
  • Exclude:
    • useNotificationSSE, useSubmissionSSE — real-time streams (not SWR targets, retain ReadableStream)
    • useAutoSave — localStorage + 30s server sync (not suited for SWR read-cache pattern)
    • useBojSearch, useProgrammersSearch — user action-driven form state integration (SWR migration would increase complexity)

Result: SWR introduction scope clarified to "declarative GET + automatic caching" purpose. Real-time/form retains existing patterns.


Outputs

New Files (7)

FileRole
frontend/src/lib/swr.tsswrFetcher + cacheKeys + invalidateAllCache
frontend/src/components/providers/SWRProvider.tsxGlobal SWRConfig (401/403/404 retry blocking, dedupingInterval 2s)
frontend/src/lib/test-utils.tsxSWRTestWrapper (test cache isolation)
frontend/src/hooks/use-problems.tsProblem list SWR hook
frontend/src/hooks/use-study-stats.tsStudy statistics SWR hook
frontend/src/hooks/use-submissions.tsSubmission list pagination SWR hook
frontend/src/hooks/use-profile-settings.tsProfile settings SWR hook

Modified Files (7)

FileChangeDescription
frontend/src/lib/api.ts+1fetchApi export added (SWR fetcher reuse)
frontend/src/app/layout.tsx+3SWRProvider inserted (StudyProvider > SWRProvider)
frontend/src/contexts/StudyContext.tsx+2invalidateAllCache call on setCurrentStudy
frontend/src/app/problems/page.tsx+12/-22useState+useEffect → useProblems+useStudyStats migration
frontend/src/app/submissions/page.tsx+31/-35useSubmissions+useProblems migration (Herald)
frontend/src/app/settings/page.tsx+26/-19useProfileSettings migration + useEffect form initialization
frontend/src/components/layout/NotificationBell.tsx+52/-7060s setInterval removed → useSWR refreshInterval (Herald)

New Test Files (4)

FileTest count
use-problems.test.tsx5
use-study-stats.test.tsx5
use-submissions.test.tsx6
use-profile-settings.test.tsx5

Modified Test Files (2)

FileChangeDescription
NotificationBell.test.tsx+90/-30notificationApi mocking → swrFetcher mocking (26 tests reactivated)
settings/__tests__/page.test.tsx+37/-20settingsApi mocking → swrFetcher mocking

Commits (6, all atomic)

2c7fd08 feat(frontend): SWR infrastructure + problems page
6f2e591 test(frontend): NotificationBell/settings test updates
192170a test(frontend): 4 SWR hook tests added (21 tests)
8c5614d refactor(frontend): NotificationBell SWR refreshInterval
1e57f71 refactor(frontend): settings page SWR
4d7425d refactor(frontend): submissions page SWR

Verification

  • npx tsc --noEmit: 0 errors
  • npm test: 1259/1259 passed (120 suites, 21 new included)
  • Coverage: All 4 new hooks cover conditional fetch/success/failure/mutate cases

Carried Over

None.

Ongoing Follow-up (Always in MEMORY.md)

  • Redis statistics cache (dashboard statistics DB direct query → cache migration)
  • problem.tags JSON column migration + seed data enrichment
  • dashboard/page.tsx SWR migration (4 parallel fetches → individual SWR hooks)
  • admin/feedbacks/page.tsx SWR migration (useSWRInfinite consideration)
  • useSWRMutation-based mutation pattern (alternative to current direct call + mutate approach)