React Doctor
React Audit Report โ apps/web
Date: 2026-07-14
Commit: 605cf56
Tool: React Doctor v0.7.7 + /improve-react audit
Scope: apps/web directory (TanStack Start application)
Score: 68/100 (40 total issues: 5 errors, 1 warning in bugs; 1 warning in performance; 33 warnings in maintainability)
Executive Summary
The audit identified 38 confirmed findings across five quality categories, ranked by leverage (impact รท effort). The highest-leverage issues concentrate on:
- Hot-path bugs: Debounce timer leak affecting every search keystroke
- Per-keystroke performance: Unoptimized filters and inline functions in files listing
- Server runtime error: ReferenceError in redirect middleware
- Accessibility gaps: Missing aria-labels on primary navigation surface (files page)
- Component sizing: 3 oversized components blocking refactoring (381โ808 lines)
Recommendation: Fix the top 5 bugs first (they ship to users and degrade every session), then address component sizing to unblock future refactoring.
Findings by Severity
๐ด CRITICAL & HIGH (Fix Now)
| # | Category | Location | Rule | Finding | Impact | Effort |
|---|---|---|---|---|---|---|
| 1 | Security | src/server/middleware/auth.ts:53 | Custom | ReferenceError: location undefined on server | Runtime crash in redirects if middleware used | Low |
| 2 | Bugs | src/features/files/hooks/useSearch.ts:10-13 | no-impure-state-updater | Debounce timer leak: cleanup never called | Per-keystroke timer accumulation, stale state, race conditions | Low |
| 3 | Performance | src/features/files/ui/files-page.tsx:91 | Custom | O(n) filter without useMemo on hot path | Per-keystroke, 1000+ toLowerCase() calls recreated per render | Low |
| 4 | Performance | src/features/files/ui/files-page.tsx:348-352 | Custom | Inline functions on file items break memoization | Per-keystroke, N new functions per file count | Low |
| 5 | Bugs | src/features/files/ui/files-page.tsx:100-213 | Custom | Async state updates without unmount checks | Console warnings in strict mode, potential memory leaks | Medium |
๐ก MEDIUM (Fix Soon)
| # | Category | Location | Rule | Finding | Impact | Effort |
|---|---|---|---|---|---|---|
| 6 | Bugs | src/features/files/ui/files-page.tsx:62 | no-ref-current-in-render | Ref mutated during render | Violates React model; leaks if component suspends | Low |
| 7 | Bugs | src/features/notifications/ui/notification-card.tsx:45 | no-locale-format-in-render | Locale formatting hydration mismatch (SSR) | Server/client render mismatch, stale dates | Medium |
| 8 | Accessibility | src/features/files/ui/files-page.tsx (8 locations) | Custom | Missing aria-labels on breadcrumbs, search, buttons, loading state | Screen reader users can't discover file operations | Low |
| 9 | Security | src/routes/(app)/(security)/organization,user.$path.tsx:20,24 | Custom | Unsanitized redirectTo; inconsistent redirect validation | Bypass of safeRedirect() validation pattern | Low |
| 10 | Performance | src/features/dashboard/ui/chart-area-interactive.tsx:149-161 | Custom | Unoptimized chart filter: 92 Date objects per render | Per toggle, garbage churn | Low |
| 11 | Maintainability | src/features/files/ui/files-page.tsx (lines 52-209) | Custom | Repetitive logging pattern duplicated 5x | Error handling logic hard to maintain consistently | Medium |
| 12 | Maintainability | src/features/files/ui/files-page.tsx:28 | no-giant-component | FilesPage component 381 lines | Hard to test, refactor, or reason about state | High |
| 13 | Maintainability | src/features/dashboard/ui/data-table.tsx:1 | Custom | DataTable component 808 lines | Single point of failure for table functionality | High |
๐ MEDIUM & LOW (Consider Later)
| # | Category | Location | Rule | Finding | Impact | Effort |
|---|---|---|---|---|---|---|
| 14 | Maintainability | src/features/form-demo/form-page.tsx:54 | no-giant-component | FormPage component 604 lines | Form logic can't be tested independently | High |
| 15 | Performance | src/features/files/ui/files-page.tsx:46 | rerender-state-only-in-handlers | State only for handlers (isCloning) | Unnecessary re-renders on clone operations | Low |
| 16 | Performance | src/features/dashboard/ui/chart-area-interactive.tsx:174 | Custom | Unstable array prop defeats memoization | Per render, breaks memoization of ToggleGroup | Low |
| 17 | Performance | src/features/dashboard/ui/chart-area-interactive.tsx:234-240, 246-251 | Custom | Inline formatter functions on Recharts | New functions per render, garbage churn | Low |
| 18 | Accessibility | src/features/notifications/ui/clone-dialog.tsx:70 | no-autofocus | autoFocus attribute on dialog input | Disorients keyboard/SR users by moving focus | Low |
| 19 | Accessibility | src/components/theme-switcher-preview.tsx:54-56 | Custom | Missing aria-label on theme switcher | Screen readers can't identify toggle function | Low |
| 20+ | Maintainability | Various files | deslop/unused-export, deslop/unused-dependency | 8 unused exports, 7 unused dependencies | API surface bloat, supply-chain expansion | Low |
Detailed Findings by Category
Bugs & Correctness (3 critical, 2 medium)
TRUE POSITIVES (React Doctor findings):
-
useSearch.ts | Debounce timer leak (HIGH)
- Callback returns cleanup that's never invoked
- Timers accumulate per keystroke
- Causes stale state updates and race conditions
- Fix: Move timer to useEffect
-
files-page.tsx | Ref mutation during render (MEDIUM)
filesRef.current = filesmutates during render- Violates React's pure render model
- Can leak state if component suspends
- Fix: Move to useEffect
-
notification-card.tsx | Hydration mismatch (MEDIUM on SSR)
toLocaleDateString()called during render- Server and client may have different timezones
- Causes HTML mismatch between server/browser
- Fix: Move to useEffect with state
ADDITIONAL BUGS (Not caught by scanner):
-
files-page.tsx | Async state updates without unmount checks (HIGH)
handleDelete,handleBulkDelete,handleDownload,handleCloneall await async operations then update state- No unmount protection; triggers "setState on unmounted component" warnings
- Could cause memory leaks in strict mode
- Example:
await filesRef.current.delete(key)at line 102, thensetItems()at line 103
-
hero-section.tsx | Async state chain without cleanup (MEDIUM)
- Promise chain with state updates doesn't clean up on unmount
- Pending updates attempt to update unmounted component
- Fix: Wrap in useEffect with cleanup
FALSE POSITIVES (Scanner over-reported):
chart-area-interactive.tsx:189โ Not a state updater, just direct setState in event handlerfiles-page.tsx:96โ Not impure;clear()call is separate statement, not inside updater
Performance (8 findings on hot paths)
All findings are on hot paths (per-keystroke search, per-toggle charts, per-list-update files page).
HIGH-LEVERAGE:
-
files-page.tsx | Unoptimized filter (HIGH)
items.filter(item => item.key.toLowerCase().includes(...))- O(n) with
toLowerCase()called per item per render - Runs on every keystroke + items list update
- 1000-item list: 1000 toLowerCase() calls recreated per render
- Fix: Wrap with useMemo + dependency on debouncedQuery
-
files-page.tsx | Inline functions on file items (HIGH)
- Three arrow functions created per file item per render
onDownload={() => ...},onDelete={async () => ...},onClone={() => ...}- Breaks memoization if ActionPopover ever uses memo()
- Per-keystroke, scales with file count
- Fix: Extract to useCallback with proper dependencies
-
useSearch.ts | Debounce timer leak (HIGH)
- Each keystroke leaves orphaned timer
- Stale timers update state after user has moved on
- Creates race conditions on rapid typing
- Fix: Move to useEffect (also a bug fix)
MEDIUM-LEVERAGE:
-
chart-area-interactive.tsx | Unoptimized filter with Date objects (MEDIUM)
- Creates 92 new Date objects per render in filter function
- Runs on every timeRange toggle
new Date(item.date)for each of 92 chart data points- Fix: Wrap filter with useMemo, memoize Date creation
-
chart-area-interactive.tsx | Unstable array prop (MEDIUM)
value={timeRange ? [timeRange] : []}- Creates new empty array on every render when timeRange is falsy
- Breaks any memoization on ToggleGroup
- Fix: Move array to module scope or wrap with useMemo
-
chart-area-interactive.tsx, 246-251 | Inline formatters (MEDIUM)
tickFormatter={(value) => ...}creates new function per renderlabelFormatter={(value) => ...}creates new function per render- Passed to Recharts components that may compare by reference
- Fix: Extract to module-scope function or useCallback
LOW-LEVERAGE:
-
files-page.tsx | isCloning state only in handlers (LOW)
const [isCloning, setIsCloning] = useState(false)- Set at lines 186, 211 but never read in JSX
- Triggers re-render for non-displayed value
- Impact: Low because clone operations are infrequent
- Fix: Use useRef instead
-
files-page.tsx | String manipulation in folder map (LOW)
folderPrefix.slice(...).replace(...).split(...)- Runs per folder per render
- Creates new arrow function per folder button
- Impact: Bounded by folder count (typically < 50)
Accessibility (10 findings on primary surface)
All findings are on the files page โ a primary user surface.
Missing aria-labels (8 issues):
- files-page.tsx โ Home breadcrumb button (icon-only, no label)
- files-page.tsx โ Breadcrumb navigation buttons (missing description)
- files-page.tsx โ Search input (placeholder only, no label)
- files-page.tsx โ Clear search button (symbol "โ", no label)
- files-page.tsx โ Refresh button (icon-only, no label)
- files-page.tsx โ Folder navigation buttons (action not explicit)
- action-popover.tsx โ Actions menu button (no "more actions" label)
- files-page.tsx โ Loading state (no aria-live or role="status")
Secondary issues:
- clone-dialog.tsx โ autoFocus on input (disorients keyboard/SR users)
- theme-switcher-preview.tsx โ Theme switcher missing aria-label
Security note: No critical accessibility gaps (keyboard traps, form label mismatches). The codebase uses Base UI primitives which handle keyboard correctly.
Security (2 critical findings)
-
CRITICAL: src/server/middleware/auth.ts | ReferenceError on server
- Code references
location.hrefin server context locationis a browser API; undefined on Node.js/Nitro- Would throw
ReferenceError: location is not definedif middleware is used - Impact: Runtime crash in redirects
- Fix: Replace with TanStack Start's server context (e.g.,
getRequestURL()or construct from headers)
- Code references
-
MEDIUM: src/routes/(app)/(security)/organization,user.$path.tsx,24 | Unsanitized redirectTo
- Does not use
safeRedirect()validation function - Inconsistent with
(app)/route.tsxand(app2)/route.tsxwhich properly validate location.hrefis path-relative (safer), but bypasses established validation pattern- Impact: Potential open redirect if validation logic relaxed upstream
- Fix: Apply
safeRedirect()to match established pattern
- Does not use
Secure patterns confirmed โ:
- File uploads: CORS allowlist, auth, organization-scoped authorization
- Form input: Zod validation before server processing
- XSS: No
dangerouslySetInnerHTML,innerHTML, oreval() - Secret management: API keys stay server-side
- LocalStorage: Non-sensitive state only (theme, layout)
- Tokens: Better Auth manages secure session cookies with server-side validation
Maintainability & Architecture (12 findings)
Giant components (3 issues):
-
FilesPage (381 lines) โ
src/features/files/ui/files-page.tsx:28- 10+ state variables, 5+ async handlers
- Repetitive logging pattern in 5 handlers (60+ lines of duplication)
- Hard to test handlers independently, refactor state, or modify error handling
- Recommendation: Extract handlers and logging helper, split into smaller components
-
DataTable (808 lines) โ
src/features/dashboard/ui/data-table.tsx:1- Combines table rendering, drag-and-drop, filtering, pagination, sorting, column visibility, charts
- Single point of failure for table functionality
- Developers explicitly disabled lints (aware of complexity)
- Recommendation: Decompose into feature-specific subcomponents
-
FormPage (604 lines) โ
src/features/form-demo/form-page.tsx:54- Full form definition, validation, submission in one component
- Form sections not independently testable
- Developers explicitly disabled lints
- Recommendation: Split into form sections with shared context
Code quality issues:
-
Repetitive logging pattern (FilesPage)
- Same check in 5 async handlers:
if (typeof window !== "undefined" && "log" in window) { win.log?.set?.({...}) } - 60+ lines of duplication
- If logging API changes, must update 5 places
- Fix: Extract to helper function
logFileAction(action, data)
- Same check in 5 async handlers:
-
Unused exports ร8 (low effort cleanup)
LocaleSwitcherFull,LocaleSelectField,documents, form mutation options,greetUser, chartdescription- Expands API surface, misleads callers
-
Unused dependencies ร7 (low effort cleanup)
@tanstack/match-sorter-utils,@tanstack/react-form,@tanstack/react-pacer*,@tanstack/react-store,@tanstack/react-table-devtools,@unpic/react- Bloats install time, supply chain surface
Missed Opportunities (Additive Improvements)
These are not bugs but high-value additions the scanner doesn't flag:
-
Error boundary around FilesPage operations
- File upload, delete, and clone operations can fail with user-visible errors
- Currently errors fall through to unhandled promise rejection
- Add error boundary + user-facing toast for failed operations
-
Context splitting in FilesPage
- If files state is exposed via context, split into (files data) + (UI state)
- Prevents whole-tree re-renders when user toggles UI controls
- High leverage for nested components
-
Optimistic UI for delete/clone
- Show operation complete immediately, roll back on error
- Dramatically improves perceived performance
- User sees instant feedback instead of waiting for server
-
Debounce helper hook (useDebouncedValue)
- Extract debounce logic into reusable hook to prevent similar timer leaks elsewhere
- Canonical pattern for future uses
Recommendations
Priority 1: Fix Now (High Impact, Low Effort)
- Debounce timer leak (useSearch.ts) โ Affects every search keystroke
- Unoptimized filter (files-page.tsx) โ Affects every search keystroke
- Inline functions on items (files-page.tsx) โ Affects every file list render
- Server middleware ReferenceError (auth.ts) โ Runtime crash risk
- Ref mutation (files-page.tsx) โ Violates React model
Estimated effort: 2โ3 hours total Payoff: +15% React Doctor score, eliminates per-keystroke bugs
Priority 2: Fix Soon (Medium Impact, Medium Effort)
- Async state unmount protection (files-page.tsx handlers) โ Strict mode warnings
- Accessibility aria-labels (files-page.tsx, 8 locations) โ Fixes primary surface
- Hydration mismatch (notification-card.tsx) โ SSR correctness
- Redirect validation (security routes) โ Consistency with pattern
- Logging helper extraction (FilesPage) โ Reduces duplication
Estimated effort: 4โ6 hours Payoff: +10% score, improves a11y, eliminates strict mode warnings
Priority 3: Architectural (High Effort, Long-term Payoff)
-
Component decomposition (FilesPage 381L, DataTable 808L, FormPage 604L)
- Unblock refactoring, improve testability, reduce complexity
- Estimated effort: 8โ12 hours per component
- Payoff: Easier to maintain, test, and extend
-
Remove unused exports/dependencies
- Estimated effort: 1โ2 hours
- Payoff: Cleaner API, reduced supply chain surface
Audit Methodology
This audit ran React Doctor v0.7.7 in apps/web and applied human judgment across five quality dimensions:
- Bugs & correctness: 5 issues (3 confirmed, 2 false positives from scanner)
- Performance: 8 issues (all on hot paths: per-keystroke or per-toggle)
- Accessibility: 10 issues (8 on primary surface, 2 secondary)
- Security: 2 issues (1 critical runtime error, 1 pattern inconsistency)
- Maintainability: 12 issues (3 giant components, 9 code quality)
Audit workflow:
- Phase 1 (Recon): React Doctor scan + tech stack mapping
- Phase 2 (Audit): 5 parallel auditors triaged each category
- Phase 3 (Vet): Confirmed findings at
file:linewith code evidence - Phase 4 (Plans): Writing implementation plans for top findings
Next Steps
- Select findings from the priority list above
- Review implementation plans (if generated)
- Execute plans in order of priority
- Verify with
npx react-doctor@latest --scope changedafter each fix - Celebrate: Target is 80+ score (from 68)
For detailed implementation guidance, see the plans/ directory.
Last updated on