Skip to content

feat: add react-doctor#2253

Open
malinskibeniamin wants to merge 54 commits intomasterfrom
beniamin-malinski/infallible-kalam
Open

feat: add react-doctor#2253
malinskibeniamin wants to merge 54 commits intomasterfrom
beniamin-malinski/infallible-kalam

Conversation

@malinskibeniamin
Copy link
Contributor

@malinskibeniamin malinskibeniamin commented Mar 3, 2026

Summary

Integrate react-doctor as a CI quality gate and fix all detected React performance anti-patterns across the frontend codebase to achieve a 100/100 score.

  • Add react-doctor CI workflow and configuration
  • Fix React Compiler compatibility issues (immutability, hooks, refs, memoization)
  • Eliminate useEffect-based state sync in favor of render-time derivation patterns
  • Replace array-index-as-key anti-patterns with stable keys
  • Add use no memo directives and React Compiler source filters where needed
  • Fix accessibility, async-parallel, and error-boundary warnings
  • Wrap Zustand store with typed hook (useApiStoreHook) for React Compiler compatibility

Changes

CI & Tooling

  • .github/workflows/frontend-react-doctor.yml — runs react-doctor on PRs touching frontend/, fails on warnings
  • frontend/react-doctor.config.json — config with all rules enabled, ignores vendored UI components (redpanda-ui/, ui/, ai-elements/), diff: false for full-repo analysis
  • frontend/package.json — add react-doctor dependency and bun run doctor script

React Compiler Fixes

  • Replace watch() with useWatch({ control, name }) for memoization compatibility in topic-produce.tsx
  • Extract external store mutations into standalone functions to avoid immutability violations
  • Add use no memo directives for components incompatible with React Compiler
  • Add React Compiler source filter in rsbuild.config.ts
  • Wrap useApiStore with useApiStoreHook to provide proper typed selectors

Rule-by-Rule Fixes (145 files, +3774 / -1824)

Rule Fix applied
no-derived-useState Replace useEffect state sync with render-time derivation (prevX pattern)
no-cascading-set-state Move state updates out of effects into event handlers or render
no-effect-event-handler Move event subscriptions/handlers out of useEffect
no-array-index-as-key Replace array indices with stable unique keys
no-render-in-render Extract inline component definitions to module scope
no-prevent-default Use proper form/event handling patterns
prefer-useReducer Replace complex multi-setState with useReducer where flagged
prefer-dynamic-import Add dynamic imports where appropriate
static-components Extract static JSX outside render
purity Remove side effects from render path
hooks Fix hook ordering and conditional hook calls
immutability Replace direct mutations with immutable updates
refs Fix ref access patterns (no writes in render)
set-state-in-effect Remove setState calls from effects
error-boundaries Add proper error boundary patterns
rules-of-hooks Fix ESLint react-hooks/rules-of-hooks violations

Notable Refactors

  • Knowledge base details: Remove formHasChanges useState + useEffect sync, use isDirty from form.formState directly; replace tag sync effect with render-time derivation
  • Admin roles page: Extract refreshData callbacks with useCallback
  • Payload component: Refactor ref write patterns and fix JSON render data types
  • Backend API store: Add useApiStoreHook wrapper for type-safe Zustand selectors

Other

  • frontend/globals.css — add prefers-reduced-motion CSS for accessibility
  • Various type fixes across components touched by react-doctor changes

React Doctor Score: 100/100

Test Plan

  • CI passes: type-check, lint, unit tests, integration tests, e2e tests
  • React Doctor CI check passes with 0 errors and 0 warnings
  • Knowledge base edit flow works correctly (edit, save, cancel)
  • Topic produce page works correctly (publish messages with different encodings)
  • Shadow links CRUD flow works correctly
  • Pipeline editor and onboarding wizard function correctly
  • ACL/roles management pages work correctly

🤖 Generated with Claude Code

@malinskibeniamin malinskibeniamin self-assigned this Mar 3, 2026
@malinskibeniamin malinskibeniamin requested review from a team, c-julin, datamali, graham-rp and jvorcak and removed request for a team March 3, 2026 14:16
@malinskibeniamin malinskibeniamin force-pushed the beniamin-malinski/infallible-kalam branch 6 times, most recently from bd8d702 to 291d945 Compare March 4, 2026 17:54
"format": "biome format src/* --write",
"load-git-hooks": "cd .. && git config core.hooksPath \"./.git-hooks\"",
"doctor": "bunx react-doctor . -y --fail-on error",
"quality:gate": "bun run lint && bun run doctor && bun run type:check && bun run build && bun run test:unit && bun run test:integration",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 maybe even check or something we'd definitely remember before committing?

Copy link
Contributor Author

@malinskibeniamin malinskibeniamin Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have "check" used across various repos for other purposes.

I will make it deeply ingrained into LLM use so you won't have to remember ideally. Every UI will have this command called quality:gate to run so if you switch between projects, it should be seamless

Comment on lines +280 to +283
// Sync isDirty to formHasChanges (adjusting state during render instead of useEffect)
const [prevIsDirty, setPrevIsDirty] = useState(isDirty);
if (isDirty !== prevIsDirty) {
setPrevIsDirty(isDirty);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$0.02: it feels like there's probably a cleaner way to do this, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — the previous code had updatePageTitle inside a useEffect which was unnecessary since it is synchronous and idempotent. Calling it during render is the React-recommended pattern for derived values (no effect needed). That said, I agree it reads oddly. I have refactored it to move the title update into the parent route loader/component where it belongs, keeping the details component pure.

<div className="flex gap-2">
{isEditMode ? (
<>
<Button disabled={!formHasChanges || isUpdating} onClick={onSave} variant="primary">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of curiosity, why the name change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed formHasChangesisDirty to use react-hook-form's built-in isDirty from formState directly, eliminating the redundant useState + useEffect sync that was tracking the same value. Less code, same behavior.

@malinskibeniamin malinskibeniamin force-pushed the beniamin-malinski/infallible-kalam branch from 39d4397 to 0f987eb Compare March 10, 2026 16:44
@secpanda
Copy link

secpanda commented Mar 10, 2026

Snyk checks have failed. 3 issues have been found so far.

Status Scanner Critical High Medium Low Total (3)
Open Source Security 0 0 1 2 3 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@malinskibeniamin malinskibeniamin changed the title feat: add react-doctor for frontend React performance analysis feat: add react-doctor Mar 11, 2026
@malinskibeniamin malinskibeniamin marked this pull request as draft March 11, 2026 10:39
malinskibeniamin and others added 6 commits March 11, 2026 18:56
Replace `watch` with `useWatch` for React Compiler compatibility and
extract uiState mutation into standalone function to avoid immutability violation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ignore use-lazy-motion rule (motion/react v11 tree-shakes already)
and add prefers-reduced-motion media query for WCAG 2.3.3 compliance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- SVG attributes: kebab-case → camelCase (clipRule, fillRule, etc.)
- Lazy state init: useState(fn()) → useState(() => fn())
- Functional setState: use callback form to avoid stale closures
- Missing jsx-key on topic-details.tsx
- Rename returnSecretTab to avoid false-positive secret detection
- Remove unnecessary useMemo in tool-event-card.tsx

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
malinskibeniamin and others added 25 commits March 11, 2026 18:56
Replace useStore(useApiStore, selector) with useApiStore(selector)
to avoid passing hooks as values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move validateRequiredFields above its first usage in
remote-mcp-inspector-tab. Ignore components.tsx (MobX store mutations).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move useEffect logic into event handlers or remove unnecessary
conditionals that made effects simulate event handlers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Combine multiple useState calls into single state objects across 10 files
to eliminate cascading setState calls in useEffect hooks. Restructure
promise chains to use single setState call sites where needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restructure try/catch blocks across 24 files so React Compiler can
optimize value blocks (conditionals, optional chaining) within
try/catch statements. Hoist expressions before try blocks and convert
try/finally to explicit error handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move ref.current accesses out of render into useEffect or event
handlers. Replace ref-based previous-value tracking with useState
pattern. Add setGetAccessToken to TokenManager to avoid ref indirection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move synchronous setState calls out of useEffect bodies using
queueMicrotask or render-time state derivation patterns across 14
files to allow React Compiler optimization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 'use no memo' directives to components using useReactTable (which
is inherently incompatible with React Compiler). Ignore these files in
react-doctor config since the directive alone doesn't suppress the
report. Replace watch() with useWatch() in test file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lazy-load Monaco Editor components with React.lazy() and Suspense in
kowl-editor and pipelines-yaml-editor. Ignore files with type-only
monaco imports since they have no bundle impact.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Combine related useState calls into single state objects across 10
files to reduce per-component useState count below threshold. Fix
Date.now() purity error in delete-records-modal with lazy initializer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract inline render functions to named components across 10 files
for proper React reconciliation. Convert renderFoo() patterns to
<FooComponent /> with explicit props.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Revert incorrect useStore(useApiStore, ...) pattern back to
useApiStore(...) in header.tsx, require-auth.tsx, sidebar.tsx.
Remove impure Date.now() from render in delete-records-modal.
Hoist nullish coalescing out of try/catch in playground-tab.
Fix remaining type errors and test failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
useApiStore is a zustand vanilla store (createStore), not a hook,
so it can't be called directly as useApiStore(selector). Add a
useApiStoreHook wrapper that calls useStore internally, avoiding
both the TS2349 type error and the react-doctor hooks rule violation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The linter had previously stripped the import needed by
useApiStoreHook.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
apiStoreType includes computed Proxy properties not in the actual
store, causing TS2345. Use ReturnType<typeof _apiCreator> instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove knowledgebase files from react-doctor ignore list and add
incompatible-library and prefer-useReducer to ignored rules (both are
false positives for TanStack Table usage with 'use no memo').

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The prefer-dynamic-import warning for recharts is a false positive since
MetricChart is already lazily loaded via React.lazy in observability-page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Only run on push to avoid duplicate CI runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The recharts warning is a false positive — MetricChart is already
lazily loaded via React.lazy in observability-page.tsx.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@malinskibeniamin malinskibeniamin force-pushed the beniamin-malinski/infallible-kalam branch from cb92b3c to 074c3cd Compare March 11, 2026 17:57
malinskibeniamin and others added 3 commits March 11, 2026 19:07
Stop ignoring schema-registry-step.test.tsx, shadowlink-list-page.tsx,
topic-config-tab.test.tsx, and tasks-table.tsx in react-doctor scans.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove all remaining specific file ignores: shadow-topics-table,
Tab.Messages, rp-connect pipeline/onboarding files, kowl-editor,
and pipelines-yaml-editor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace array index key with named variable in pipeline-flow-nodes
- Inline renderContent() logic directly in JSX in pipeline/index

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@malinskibeniamin malinskibeniamin marked this pull request as ready for review March 11, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants