Metadata
IMPORTANT: The very first step should ALWAYS be validating this metadata section to maintain a CLEAN development workflow.
pull_request_title: "feat: eliminate direct useEffect — adopt declarative React patterns"
branch: "feat/useeffect-refactor"
User Stories
- As a developer, I want useEffect calls replaced with declarative patterns (TanStack Query, useMountEffect, derived state, key props) so that the codebase has fewer hidden coupling issues, infinite loops, and race conditions.
- As a developer, I want a
useMountEffect hook for one-time setup so that mount-only effects are semantically clear and the eslint suppression is centralized.
- As a developer, I want data fetching managed by TanStack Query so that loading/error states, caching, and cache invalidation are handled declaratively.
Summary
Based on the "Why we banned useEffect" approach, this issue tracks refactoring ~118 direct useEffect calls across 83 frontend files into 5 declarative patterns:
- Derive state, don't sync it —
useMemo / inline calculation
- Use data-fetching libraries — TanStack Query (
@tanstack/react-query)
- Event handlers, not effects — handle actions directly in handlers
useMountEffect for external sync — semantic wrapper for useEffect(fn, [])
- Reset with
key, not choreography — React key prop for clean remounts
Visual Reference
Tweet: https://x.com/alvinsng/status/2033969062834045089
Key Integration Points
| File |
Function(s) |
Role |
frontend/src/hooks/useAuth.tsx |
useEffectGetUser() |
Auth state — replace with useQuery |
frontend/src/hooks/useModel.tsx |
useModelsEffect() |
Model list — replace with useQuery |
frontend/src/hooks/useAgent.ts |
useEffectGetAgents(), state-sync effect |
Agent data + model sync — replace with useQuery, remove sync |
frontend/src/hooks/useThread.ts |
useListThreadsEffect(), useLoadThreadEffect() |
Thread data — replace with useQuery/useInfiniteQuery |
frontend/src/hooks/useChat.ts |
useEffectUpdateAssistantId() |
Metadata sync — remove (redundant with getMetadata()) |
frontend/src/context/ChatContext.tsx |
7 effects (file sync, autosave, chains) |
Effect chains — break with useMemo, useMountEffect, custom hooks |
UI Integration Points
| Component / Route |
Change Type |
Description |
src/components/settings/*Settings.tsx (6 files) |
Modify |
Replace fetch effects with useQuery |
src/pages/threads/ThreadPage.tsx |
Modify |
Key-reset pattern with key={threadId} |
src/components/panels/FileEditorPanel.tsx |
Modify |
Derive state instead of syncing via effects |
src/pages/agents/edit.tsx, thread.tsx |
Modify |
Key-reset + useMountEffect for cleanup |
Storage
- Persistence layer: No changes — existing API services remain the data layer
- Caching: TanStack Query in-memory cache replaces manual
useState for server data
- Model pattern: Existing service layer (
src/lib/services/) unchanged
Architectural Decisions
- Source of truth: TanStack Query cache for server state, React state for UI state
- State management:
useQuery for reads, useMutation + invalidateQueries for writes
- No Redux/Zustand: Continue using React Context for cross-cutting UI state
- Incremental rollout: 7 phases across 6-9 PRs, each self-contained and revertible
Documentation
Development Setup
Dependencies
| Dependency |
Version |
Notes |
@tanstack/react-query |
v5 |
Core data-fetching library |
@tanstack/react-query-devtools |
v5 |
Dev-only debugging panel |
Commands
cd frontend && npm install
npm run test
npm run dev:claude
Design Principles
- Simplicity is beauty, complexity is pain.
- ALWAYS look at the current codebase first — achieve the goal in the least amount of changes.
- TDD-first: write tests before implementation.
- Effects are for synchronizing with external systems, not for deriving state or fetching data.
- Every
useEffect must justify its existence — if it can be a useMemo, event handler, or key prop, it should be.
Validation Tools
Acceptance Criteria
Metadata
User Stories
useMountEffecthook for one-time setup so that mount-only effects are semantically clear and the eslint suppression is centralized.Summary
Based on the "Why we banned useEffect" approach, this issue tracks refactoring ~118 direct
useEffectcalls across 83 frontend files into 5 declarative patterns:useMemo/ inline calculation@tanstack/react-query)useMountEffectfor external sync — semantic wrapper foruseEffect(fn, [])key, not choreography — Reactkeyprop for clean remountsVisual Reference
Tweet: https://x.com/alvinsng/status/2033969062834045089
Key Integration Points
frontend/src/hooks/useAuth.tsxuseEffectGetUser()useQueryfrontend/src/hooks/useModel.tsxuseModelsEffect()useQueryfrontend/src/hooks/useAgent.tsuseEffectGetAgents(), state-sync effectuseQuery, remove syncfrontend/src/hooks/useThread.tsuseListThreadsEffect(),useLoadThreadEffect()useQuery/useInfiniteQueryfrontend/src/hooks/useChat.tsuseEffectUpdateAssistantId()getMetadata())frontend/src/context/ChatContext.tsxuseMemo,useMountEffect, custom hooksUI Integration Points
src/components/settings/*Settings.tsx(6 files)useQuerysrc/pages/threads/ThreadPage.tsxkey={threadId}src/components/panels/FileEditorPanel.tsxsrc/pages/agents/edit.tsx,thread.tsxuseMountEffectfor cleanupStorage
useStatefor server datasrc/lib/services/) unchangedArchitectural Decisions
useQueryfor reads,useMutation+invalidateQueriesfor writesDocumentation
Development Setup
Dependencies
@tanstack/react-query@tanstack/react-query-devtoolsCommands
Design Principles
useEffectmust justify its existence — if it can be auseMemo, event handler, orkeyprop, it should be.Validation Tools
agent-browserskill with screenshots to validate E2Enpm run testpasses after each phasenpm run lintpassesAcceptance Criteria
useMountEffecthook created and used for all mount-only effectssrc/lib/queryKeys.tsuseEffectXxx()wrapper patterns eliminated from hooksuseQueryinstead of manual fetch effectskey={threadId}instead of state-reset effectuseMemo+useMountEffect