Skip to content

fix(react-doctor): clear state and effect warnings#345

Open
bntvllnt wants to merge 10 commits into
mainfrom
fix/271-state-effects
Open

fix(react-doctor): clear state and effect warnings#345
bntvllnt wants to merge 10 commits into
mainfrom
fix/271-state-effects

Conversation

@bntvllnt
Copy link
Copy Markdown
Collaborator

@bntvllnt bntvllnt commented May 13, 2026

Summary

  • Clears the react-doctor: state & effects — derived state, cascading setState, effect-event handlers (41 warnings) #271 state/effects React Doctor rule family by replacing derived default state, effect-owned event handlers, and effect-driven state sync patterns.
  • Adds and publicly exports React 18-compatible state/event helper hooks (useControllableState, useUncontrolledState, useEventCallback, useDocumentEventListener, useWindowEventListener) so registry examples can import them from @vllnt/ui.
  • Regenerates the affected registry component copies through the build pipeline.
  • Refreshes the branch on top of latest origin/main via merge commit ea54461.
  • Fixes the MapTimeline playback end-state regression so RAF playback explicitly clears isPlaying at endYear, with focused button-state coverage.
  • Fixes the current-head useControllableState functional-updater regression by preserving latest uncontrolled state across batched updater calls, with focused regression coverage.
  • Removes shipped inline // comments from CookieConsent source and registry copy.
  • Fixes the CookieConsent uncontrolled-dismiss regression by wiring the package and registry copies through useControllableState; default uncontrolled Accept/Close now remove the dialog after the exit animation while controlled open + onOpenChange remains supported.

Validation

Current head 9d1a09b6fa1e63a0c1e407aee967b392ebe88f44:

  • git diff --check origin/main...HEAD
  • git diff --check
  • pnpm -F @vllnt/ui exec vitest run src/components/cookie-consent/cookie-consent.test.tsx (1 file passed, 30 tests passed)
  • pnpm -F @vllnt/ui exec tsc --noEmit --project tsconfig.build.json
  • pnpm -F @vllnt/ui lint
  • pnpm build
  • pnpm test:once (217 test files passed, 1219 tests passed; known jsdom navigation, ShareDialog Description, and InteractiveTimeline duplicate-key warnings still print)

Prior validation retained from earlier heads:

  • pnpm -F @vllnt/ui exec vitest run src/lib/use-uncontrolled-state.test.tsx (1 test passed)
  • pnpm -F @vllnt/ui exec vitest run src/components/map-timeline/map-timeline.test.tsx (9 tests passed)
  • pnpm -F @vllnt/ui-registry exec tsc --noEmit --project tsconfig.json
  • npx --yes react-doctor . --offline --json (targeted react-doctor: state & effects — derived state, cascading setState, effect-event handlers (41 warnings) #271 state/effects rules = 0)
  • npx --yes react-doctor . --diff origin/main --annotations --fail-on error --offline (exits 0; non-target warnings remain from other tracked issue families)
  • Prior public helper export smoke compile using a temp TS file importing all five helpers from packages/ui/src/index.ts.

Review / CI

  • Current head: 9d1a09b6fa1e63a0c1e407aee967b392ebe88f44.
  • Local workspace gates above are green at current head.
  • Current-head GitHub Actions checks are green: Quality Gates, CodeQL, issue-link, Storybook verify/build/tests/visual, react-doctor, build/deploy, and preview deploy passed.
  • GitHub still reports one unnamed external status pointing at https://vllnt.ai as failing; this is outside the repository Actions checks surfaced for the PR and appears unrelated to the code changes.
  • Remaining state: open/non-draft; pending fresh reviewer or maintainer confirmation after this blocker remediation.

Notes

  • Kept the event helper React 18-compatible instead of using React 19 useEffectEvent because @vllnt/ui peers React >=18.0.0.
  • Build still prints the known Next.js multiple-lockfile root warning in worktrees.
  • Tests still print known jsdom navigation, ShareDialog Description, and InteractiveTimeline duplicate-key warnings.
  • Codex CLI was attempted for the remediation but the installed CLI could not authenticate (401 Unauthorized), so the code mutation was delegated to a Hermes subagent in the dedicated PR worktree and then independently validated before the non-force push.

Closes #271

@bntvllnt bntvllnt self-assigned this May 13, 2026
@bntvllnt bntvllnt added bug Something isn't working react-doctor Reported by react-doctor (codebase health) tech-debt Refactoring or cleanup labels May 13, 2026
Copy link
Copy Markdown
Collaborator Author

@bntvllnt bntvllnt left a comment

Choose a reason for hiding this comment

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

REVIEW FALLBACK (would REQUEST_CHANGES, but GitHub rejected request-changes because the authenticated reviewer owns the PR).

Requesting changes: I found a blocking registry-snippet regression in the new shared hook imports.

The package implementation refactor itself is mostly sound from a state/effect perspective: I inspected the 41 changed files, including the new useEventCallback, useDocumentEventListener, useWindowEventListener, useUncontrolledState, and useControllableState helpers and the components migrated to them. The issue is that multiple copied registry default snippets now import these helpers from the public @vllnt/ui root, but those helper symbols are not exported from packages/ui/src/index.ts / the package root export surface.

Why this blocks:

  • The registry app tsconfig excludes registry/**, so the normal registry typecheck does not cover copied snippets.
  • A targeted snippet typecheck reproduces the public-import failures, e.g. Module '"@vllnt/ui"' has no exported member 'useControllableState', useEventCallback, and useDocumentEventListener.
  • The affected snippets are copied consumer-facing code, so users who install/use these registry examples will get compile errors.

Evidence preserved in the Kanban workspace:

  • artifacts/pr345-hook-inventory.txt — all 41 changed files inventoried for hook usage.
  • artifacts/validation/registry-snippet-public-import-tsc.txt — targeted snippet typecheck showing the missing public exports.
  • artifacts/validation/registry-tsc-symlink.txt — normal registry tsc passed only because registry/** is excluded.
  • Required gates attempted: ui-lint.exit=1, ui-tsc.exit=1, workspace-build.exit=0, workspace-test-once.exit=1. The lint/tsc/test failures appear tied to existing/missing dependency resolution around form/react-hook-form in this symlinked detached worktree, while workspace build passed from cache.

Please either export these new hook helpers from the public @vllnt/ui root, or keep the registry snippets self-contained / avoid importing non-public helpers from @vllnt/ui.


import { Bot, MessageSquarePlus, X } from "lucide-react";

import { useControllableState } from "@vllnt/ui";
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This registry snippet now imports useControllableState from the public @vllnt/ui root, but that symbol is not exported by the package root (packages/ui/src/index.ts). The same pattern appears in the other migrated snippets for useEventCallback, useDocumentEventListener, useWindowEventListener, and useUncontrolledState. Because apps/registry/tsconfig.json excludes registry/**, the regular registry typecheck does not catch it; a targeted snippet typecheck fails with TS2305 missing-export errors. Please either export these helpers publicly or keep the registry snippets self-contained so copied examples compile for consumers.

Copy link
Copy Markdown
Collaborator Author

@bntvllnt bntvllnt left a comment

Choose a reason for hiding this comment

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

Review complete at ea54461. I would REQUEST_CHANGES, but GitHub rejects request-changes reviews on my own PR/auth identity. The original registry import blocker is fixed by exporting the helper hooks and the targeted registry typecheck passes, but the MapTimeline playback regression below remains blocking. Also note the app Vercel preview remains failed/canceled under the existing waiver; not treating that as code approval.

if (next >= endYear) {
setYear(endYear);
setIsPlaying(false);
return;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Blocking: this return leaves isPlaying stuck at true after playback reaches endYear. The RAF loop stops because no next frame is scheduled, but MapTimelinePlayButton still renders the pause state (aria-pressed=true, pause label/icon), so users must click once to flip to stopped before they can play again. Please restore the previous setIsPlaying(false) behavior (or otherwise clear playing state) when clamping to endYear.

bntvllnt added 2 commits May 17, 2026 05:17
Keep the registry MapTimeline copy in parity with source behavior by clearing playback when RAF reaches the configured end year.
Copy link
Copy Markdown
Collaborator Author

@bntvllnt bntvllnt left a comment

Choose a reason for hiding this comment

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

REVIEW FALLBACK (would REQUEST_CHANGES, but GitHub rejects request-changes reviews on my own PR/auth identity).

Review — 1 blocking metadata finding

BLOCKING

  • C1 — PR body/check evidence is stale for the current head.
    • Evidence: current PR head is 767172124bd9c08ebb85285566ff8b98e253dfd1, but the PR body still says Current head: 9a3d134e14486717928be663bcce50674609d949 and says checks are re-running for 9a3d134e14486717928be663bcce50674609d949.
    • Rule: docs/agents/RULES.md R3 — after every push, rewrite the PR body so validation commands, screenshots/recordings, CI status, and listed components match the current head; stale claims block ship.
    • Fix: refresh the PR body for 767172124bd9c08ebb85285566ff8b98e253dfd1, including the MapTimeline registry parity push and current check status.

VERIFIED CLEAN

  • Reviewed the latest-push delta at 767172124bd9c08ebb85285566ff8b98e253dfd1: apps/registry/registry/default/map-timeline/map-timeline.tsx now clears isPlaying when RAF playback reaches endYear (setYear(endYear); setIsPlaying(false); return;).
  • Re-checked source/registry parity for MapTimeline playback behavior: packages/ui/src/components/map-timeline/map-timeline.tsx and the registry copy now match for the useTimelineState and usePlayback end-year stop behavior. The only full-file diff between source and registry is the expected import path (../../lib/utils vs @vllnt/ui).
  • The focused regression test exists in packages/ui/src/components/map-timeline/map-timeline.test.tsx and asserts that playback returns to the Play state after advancing beyond the end year.

VALIDATION

  • Ran on reviewed head 767172124bd9c08ebb85285566ff8b98e253dfd1:
    • git diff --check origin/main...HEAD — pass
    • pnpm -F @vllnt/ui exec vitest run src/components/map-timeline/map-timeline.test.tsx — pass, 9 tests
    • pnpm -F @vllnt/ui-registry exec tsc --noEmit --project tsconfig.json — pass
  • Observed GitHub checks at review time: CodeQL, Build Storybook, Analyze (actions), Verify Stories, Scan codebase health, Analyze (javascript-typescript), and Enforce issue-linked PRs were passing; Storybook Tests, Visual Regression, and Quality Gates were still in progress/pending.

Copy link
Copy Markdown
Collaborator Author

@bntvllnt bntvllnt left a comment

Choose a reason for hiding this comment

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

Review — metadata blocker cleared

BLOCKING

  • None.

VERIFIED CLEAN

  • Re-fetched PR #345 live and confirmed the head is still 767172124bd9c08ebb85285566ff8b98e253dfd1 on fix/271-state-effects.
  • The PR body no longer cites the stale 9a3d134e current-head/check rerun evidence, the old mixed ea54461 check evidence, or the canceled Vercel note that caused the prior metadata blocker.
  • The remaining ea54461 mention is limited to the branch-history summary (Refreshes the branch on top of latest origin/main via merge commit ea54461), not stale CI/head evidence.
  • Current GitHub checks observed for the head are green: Quality Gates, Analyze (actions), Enforce issue-linked PRs, Verify Stories, Scan codebase health, Analyze (javascript-typescript), Build Storybook, Storybook Tests, Visual Regression, and CodeQL all report success.
  • Live merge state is open, non-draft, mergeable, and clean.

VALIDATION

  • Re-fetched live PR metadata/body/checks/reviews through GitHub CLI/API.
  • Compared the refreshed body against the stale metadata strings from the prior blocker.
  • No code, push, merge, approval, close, release, or production mutation was performed.

Verdict: COMMENT only. Approval is recommended from this gate, but final approval remains reserved for the maintainer/bntvllnt.

@vllnt-pilot vllnt-pilot Bot had a problem deploying to Preview · pr-345-storybook May 18, 2026 17:14 Failure
@vllnt-pilot
Copy link
Copy Markdown

vllnt-pilot Bot commented May 18, 2026

Preview ready · Updated 2026-05-20T16:10:03Z

Service Status Preview
ui-registry Ready https://pr-345-ui-registry.preview.vllnt.ai
Inspect
  • Tailnet-only by default (not reachable from public internet)
  • Cert: real Let's Encrypt wildcard
  • Reply with /clean to destroy this preview now

@vllnt vllnt deleted a comment from vllnt-pilot Bot May 18, 2026
@vllnt vllnt deleted a comment from vercel Bot May 18, 2026
Copy link
Copy Markdown
Collaborator Author

@bntvllnt bntvllnt left a comment

Choose a reason for hiding this comment

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

REVIEW FALLBACK (would REQUEST_CHANGES, but GitHub does not allow request-changes reviews from the PR author account).

Blocking issues found at HEAD edda95c:

  1. PR body is stale and violates the repo R3 rule that the PR body must match HEAD. gh pr view reports current head edda95c9234aac2c4b668e315c6d74eb79ab6376, but the body still says Current head: 767172124bd9c08ebb85285566ff8b98e253dfd1 and presents validation for that older revision. Please refresh the body/validation section for the actual current head.

  2. Inline findings below: useControllableState does not preserve the Dispatch<SetStateAction<T>> semantics it exposes, and the PR adds an inline // comment in shipped component code.

Remote checks were green at review time. I did not merge or push anything.

(nextValue) => {
const resolvedNextValue =
typeof nextValue === "function"
? (nextValue as (currentValue: T) => T)(resolvedValue)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This helper returns Dispatch<SetStateAction<T>>, but functional updates are resolved against the render-captured resolvedValue before calling React state. In uncontrolled mode, two batched calls such as setValue(n => n + 1); setValue(n => n + 1); both compute from the same stale value and only increment once, unlike useState/Dispatch semantics promised by the type and docs. This line also introduces an as assertion in shipped TS, which the repo rules prohibit. Please preserve updater semantics by forwarding the updater to React state in the uncontrolled branch (and derive the onChange value from the current state there), or narrow the API so it does not claim Dispatch<SetStateAction<T>>.


const closeTimerRef = useRef<null | ReturnType<typeof setTimeout>>(null);

// Clear close timer on unmount to avoid dispatching into an unmounted component.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Repo rules prohibit inline // comments in shipped TS/TSX. This PR adds one here (and the generated registry copy repeats it), so please remove it or make the code self-explanatory/TSDoc-only before merging.

};
}, [open]);

const handleClose = useCallback(() => {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Blocking: this new registry example does not dismiss itself in the default uncontrolled usage. handleClose only calls onOpenChange?.(false) after the animation, but open defaults to true and the render guard is still if (!open && !isAnimatingOut) return null. Consumers who install/use the registry component without controlling open will see Accept/Decline/Close animate out and then remain mounted. Please add internal closed state or use the shared controllable-state helper so the default registry path actually closes.

@bntvllnt
Copy link
Copy Markdown
Collaborator Author

Jarvis review fallback: request changes

Formal REQUEST_CHANGES review submission was rejected because the authenticated account is the PR author, so this issue comment plus the inline discussion at apps/registry/registry/default/cookie-consent/cookie-consent.tsx:138 is the authoritative review artifact for head 0b6662879b8483280038cd255d6a980a13c92d3b.

Blocking

  • apps/registry/registry/default/cookie-consent/cookie-consent.tsx:138: the newly added registry example does not dismiss itself in the default uncontrolled usage. handleClose only starts the exit animation and then calls onOpenChange?.(false), while the render guard still depends on the open prop (if (!open && !isAnimatingOut) return null). Because open defaults to true, using the registry component without a controlled open/onOpenChange pair means Accept/Decline/Close animate out and then remain mounted/in the DOM. Please give the component an internal uncontrolled closed state, or switch it to the shared controllable-state helper so the default registry example actually dismisses after user action.

Coverage / validation checked

  • Reviewed all 43 changed files at current head. Coverage ledger: /home/ubuntu/.hermes/kanban/boards/vllnt-platform/workspaces/t_ee33d9b3/artifacts/coverage-ledger.md.
  • git diff --check origin/main...HEAD: pass.
  • pnpm -F @vllnt/ui lint: pass locally after temporary worktree node_modules symlink bootstrap from the installed main checkout.
  • pnpm -F @vllnt/ui exec tsc --noEmit --project tsconfig.build.json: pass locally with the same bootstrap.
  • pnpm -F @vllnt/ui exec vitest run src/lib/use-uncontrolled-state.test.tsx src/components/map-timeline/map-timeline.test.tsx src/components/cookie-consent/cookie-consent.test.tsx: 38 tests passed locally with the same bootstrap.
  • Live GitHub checks at this head are green/neutral: Quality Gates, CodeQL, issue link, Storybook/build/visual, react-doctor, preview deploy.

The rest of the changed hook/state refactors did not surface additional blockers in this pass.

@bntvllnt
Copy link
Copy Markdown
Collaborator Author

Review result: fallback comment (GitHub rejected a formal APPROVE because this PR is authored by the authenticated account)

CookieConsent remediation looks correct at current head 9d1a09b.

What I checked:

  • The package CookieConsent no longer defaults the controlled open prop to true; it now leaves open undefined in uncontrolled mode and derives openState through useControllableState({ defaultValue: true, value: open, onChange: onOpenChange }).
  • The registry copy mirrors the same controlled/uncontrolled behavior and imports the exported helper from @vllnt/ui.
  • Accept, decline, and close all go through the same 200ms exit path, with uncontrolled mode updating internal state and controlled mode still notifying onOpenChange(false) without forcing parent state.
  • Regression coverage now proves the default uncontrolled banner is removed after Accept and close-button animation, and existing controlled callback coverage remains in place.

Validation I ran locally from a detached review worktree:

  • git diff --check origin/main...HEAD
  • git diff --check
  • pnpm -F @vllnt/ui exec vitest run src/components/cookie-consent/cookie-consent.test.tsx src/lib/use-uncontrolled-state.test.tsx ✅ 2 files / 31 tests
  • pnpm -F @vllnt/ui exec tsc --noEmit --project tsconfig.build.json
  • pnpm -F @vllnt/ui lint
  • pnpm build
  • pnpm test:once

CI evidence:

  • GitHub Actions checks visible on the PR are green, including Quality Gates, CodeQL, issue-link, Storybook, react-doctor, build/deploy, and preview deploy.
  • GitHub still reports one unnamed external status pointing at https://vllnt.ai as failing; I did not find that tied to this CookieConsent remediation or the repository Actions checks, but it should still be accounted for before merge if it is a required branch-protection status.

No blocking code findings from this review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working react-doctor Reported by react-doctor (codebase health) tech-debt Refactoring or cleanup

Projects

None yet

Development

Successfully merging this pull request may close these issues.

react-doctor: state & effects — derived state, cascading setState, effect-event handlers (41 warnings)

1 participant