|
| 1 | +# React Doctor — Web App Baseline Audit |
| 2 | + |
| 3 | +**Date:** 2026-02-21 |
| 4 | +**Scope:** `apps/web/src/` — 6 React components, 5 lib modules, 3 test files |
| 5 | +**React version:** 19.2.4 | **Astro version:** 5.2.0 |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## Overall Score: 78/100 |
| 10 | + |
| 11 | +| Category | Score | Max | Notes | |
| 12 | +|----------|-------|-----|-------| |
| 13 | +| Hook Hygiene | 9 | 10 | All effects are external-system sync; no anti-patterns | |
| 14 | +| State Management | 8 | 10 | Minimal state; minor: URL parsing on every render | |
| 15 | +| Performance | 7 | 10 | Index keys in output, uncleaned timeouts, all `client:load` | |
| 16 | +| Accessibility | 6 | 10 | Good labels/ARIA on forms; missing on interactive widgets | |
| 17 | +| TypeScript Strictness | 8 | 10 | Strict mode; two unsafe `as unknown` casts | |
| 18 | +| Test Coverage | 6 | 10 | Utilities fully tested; zero component-level tests | |
| 19 | +| Error Handling | 9 | 10 | Proper user-facing errors; no ErrorBoundary | |
| 20 | +| Code Organization | 9 | 10 | Clean separation, co-located tests, focused components | |
| 21 | +| Security | 8 | 10 | Credentials include, key shown once; clipboard lacks error handling | |
| 22 | +| Bundle Efficiency | 8 | 10 | All dashboard components eagerly loaded (`client:load`) | |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## Issues Found |
| 27 | + |
| 28 | +### P1 — Should Fix |
| 29 | + |
| 30 | +#### 1. Missing `aria-expanded` on ExecCard toggle |
| 31 | +**File:** `components/replay/ReplayViewer.tsx:61-65` |
| 32 | +**Category:** Accessibility |
| 33 | +The exec card expand/collapse button uses a chevron (`▾`/`▸`) but lacks `aria-expanded` to communicate state to assistive tech. |
| 34 | + |
| 35 | +#### 2. Missing `aria-label` on error icon |
| 36 | +**File:** `components/replay/ReplayViewer.tsx:186` |
| 37 | +**Category:** Accessibility |
| 38 | +The lock emoji (`🔒`) used as an error icon has no `aria-label` or `role="img"`. Screen readers will announce the raw Unicode character. |
| 39 | + |
| 40 | +#### 3. Missing label on invite email input |
| 41 | +**File:** `components/dashboard/OrgSettings.tsx:232` |
| 42 | +**Category:** Accessibility |
| 43 | +The invite email `<input>` has no associated `<label>` or `aria-label`. Placeholder text alone is insufficient for accessibility. |
| 44 | + |
| 45 | +#### 4. Uncleaned `setTimeout` in ApiKeyManager |
| 46 | +**File:** `components/dashboard/ApiKeyManager.tsx:96` |
| 47 | +**Category:** Performance / Correctness |
| 48 | +`setTimeout(() => setCopied(false), 2000)` is never cleared. If the component unmounts within 2s, this calls `setState` on an unmounted component. |
| 49 | + |
| 50 | +#### 5. Uncleaned `setTimeout` in OrgSettings |
| 51 | +**File:** `components/dashboard/OrgSettings.tsx:81` |
| 52 | +**Category:** Performance / Correctness |
| 53 | +Same pattern: `setTimeout(() => setUpdateSuccess(false), 3000)` is never cleared. |
| 54 | + |
| 55 | +### P2 — Nice to Fix |
| 56 | + |
| 57 | +#### 6. URL parsing on every render in VerifyOtpForm |
| 58 | +**File:** `components/auth/VerifyOtpForm.tsx:13-16` |
| 59 | +**Category:** Performance |
| 60 | +`new URLSearchParams(window.location.search)` is called on every render. Since the URL doesn't change during this component's lifetime, this should be computed once (e.g., outside the component or in a `useMemo`). |
| 61 | + |
| 62 | +#### 7. Unsafe type casts in OrgSettings |
| 63 | +**File:** `components/dashboard/OrgSettings.tsx:51-53` |
| 64 | +**Category:** TypeScript Strictness |
| 65 | +Triple `as unknown as X` casts bypass TypeScript entirely. These should use BetterAuth's own types or a validated transform. |
| 66 | + |
| 67 | +#### 8. Index keys for output entries |
| 68 | +**File:** `components/replay/ReplayViewer.tsx:89` |
| 69 | +**Category:** Performance |
| 70 | +Output entries use `key={i}` (array index). These are append-only and immutable after load, so impact is minimal, but a content hash or entry timestamp would be more robust. |
| 71 | + |
| 72 | +#### 9. All dashboard pages use `client:load` |
| 73 | +**Files:** `pages/dashboard/*.astro` |
| 74 | +**Category:** Bundle Efficiency |
| 75 | +All three dashboard pages use `client:load` which eagerly hydrates React. `client:idle` would defer hydration until the browser is idle, improving perceived performance on slower devices. |
| 76 | + |
| 77 | +### P3 — Low Priority |
| 78 | + |
| 79 | +#### 10. No React ErrorBoundary |
| 80 | +**Category:** Error Handling |
| 81 | +No `ErrorBoundary` wraps any React island. An unhandled error in a React component will crash the entire island with a white screen. |
| 82 | + |
| 83 | +#### 11. Empty `<th>` elements without scope |
| 84 | +**Files:** `SandboxList.tsx:121`, `ApiKeyManager.tsx:174`, `OrgSettings.tsx:205` |
| 85 | +**Category:** Accessibility |
| 86 | +Action columns have empty `<th>` elements. Adding `<th scope="col"><span class="sr-only">Actions</span></th>` would help screen readers. |
| 87 | + |
| 88 | +#### 12. `mobile-dash-btn` missing `aria-expanded` |
| 89 | +**File:** `layouts/DashboardLayout.astro:44` |
| 90 | +**Category:** Accessibility |
| 91 | +The mobile menu toggle button has `aria-label` but not `aria-expanded`. State should be toggled in the click handler. |
| 92 | + |
| 93 | +--- |
| 94 | + |
| 95 | +## What's Working Well |
| 96 | + |
| 97 | +- **Zero useEffect anti-patterns.** All 5 effects are external-system sync (data fetching, focus management, redirects). No derived-state-in-effects, no chained effects, no effects-to-notify-parent. |
| 98 | +- **Proper async cancellation.** `ReplayViewer` uses a `cancelled` flag in its fetch effect cleanup — correct pattern. |
| 99 | +- **Good accessibility baseline.** Forms use `htmlFor`/`id` pairing, `role="alert"` for errors, `role="status"` for success, `aria-label` on OTP inputs. |
| 100 | +- **Clean state management.** Components use minimal `useState`. `Set<string>` for tracking in-flight mutations is a good pattern. |
| 101 | +- **Security-conscious API key handling.** Key shown once, then masked. Good UX + security. |
| 102 | +- **Utility test coverage.** All pure functions in `lib/` have thorough tests with edge cases. |
| 103 | +- **No barrel re-exports or circular dependencies.** |
| 104 | +- **ESM imports with `.js` extensions** per project convention. |
| 105 | + |
| 106 | +--- |
| 107 | + |
| 108 | +## Typecheck Findings |
| 109 | + |
| 110 | +`astro check` — **0 errors, 0 warnings, 7 hints**: |
| 111 | + |
| 112 | +- 2x unused `weak` variable in `BentoGrid.astro:147` and `Cta.astro:147` (canvas animation helpers) |
| 113 | +- 5x deprecated `React.FormEvent` in React 19 — should migrate to `React.FormEvent` from `react` namespace directly (affects EmailForm, VerifyOtpForm, ApiKeyManager, OrgSettings) |
| 114 | + |
| 115 | +--- |
| 116 | + |
| 117 | +## Test Suite |
| 118 | + |
| 119 | +`react-doctor.test.ts` — **68 tests, all passing** |
| 120 | + |
| 121 | +The test suite programmatically audits all `.tsx` components for: |
| 122 | +- useEffect anti-patterns (derived state, chained effects, notify-parent) |
| 123 | +- Raw string throws |
| 124 | +- `any` type annotations |
| 125 | +- `console.log` in production code |
| 126 | +- Form input accessibility (labels/aria-label) |
| 127 | +- Icon button accessibility |
| 128 | +- Error message `role="alert"` usage |
| 129 | +- List key patterns |
| 130 | +- Inline style counts |
| 131 | +- useCallback dependency arrays |
| 132 | +- Baseline score tracking |
| 133 | + |
| 134 | +Known issues are documented as expected baseline values in the test, so regressions will cause failures and improvements can be tracked by updating the expected values. |
| 135 | + |
| 136 | +--- |
| 137 | + |
| 138 | +## Recommendations (Prioritized) |
| 139 | + |
| 140 | +1. Add `aria-expanded` to all toggle buttons (ExecCard, mobile menu) |
| 141 | +2. Add labels to all form inputs (invite email) |
| 142 | +3. Add `role="alert"` to dashboard error messages (3 components) |
| 143 | +4. Clean up `setTimeout` calls with `useEffect` return or `useRef` for timer IDs |
| 144 | +5. Add a root `ErrorBoundary` to catch rendering errors in React islands |
| 145 | +6. Move URL parsing out of render path in VerifyOtpForm |
| 146 | +7. Replace `as unknown` casts with proper BetterAuth types |
| 147 | +8. Consider `client:idle` for non-critical dashboard pages |
| 148 | +9. Add component-level tests for key user flows (OTP input, sandbox list) |
0 commit comments