Skip to content

fix(channels): read checkbox state before the setState updater runs (#5161) - #5279

Merged
M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/5161-null-checked-ref
Jul 31, 2026
Merged

fix(channels): read checkbox state before the setState updater runs (#5161)#5279
M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/5161-null-checked-ref

Conversation

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Summary

  • Fix TypeError: Cannot read properties of null (reading 'checked') (Sentry TAURI-REACT-39, ~8 events / ~7 users across 4 shortIds).
  • Root cause: the "also delete memory" checkbox in the Telegram and Discord config panels read event.currentTarget.checked from inside a functional setState updater, which React invokes after it has already nulled currentTarget.
  • Read checked synchronously in the handler and let the updater close over the captured value — three sites (Telegram ×1, Discord ×2).
  • Drop an unreachable try/catch in analyticsInteractions.ts that misattributed this issue to a disconnected DOM node.
  • Regression tests reproduce the exact production error before the fix, one case per patched render branch.

Problem

The memory checkbox in both channel config panels was wired like this:

onChange={event =>
  setClearMemoryOnDisconnect(prev => ({
    ...prev,
    [compositeKey]: event.currentTarget.checked,
  }))
}

React resets event.currentTarget to null as soon as the handler returns. A functional updater is not evaluated inside the handler — React invokes it later, while processing the update queue during render. The read therefore landed on a nulled currentTarget and threw.

Why it only failed sometimes (~8 events across ~7 users, rather than on every toggle): React evaluates an updater eagerly inside dispatchSetState while the fiber has no pending work, as a bail-out optimisation. On that path the read still happens inside the handler's synchronous window and succeeds. As soon as any other update was already queued — a second toggle, a concurrent render — the eager path is skipped, the updater runs at render time, and currentTarget is gone. This also explains the useState frame in the Sentry stack: the throw happens inside React's update-queue processing, not in the event handler.

Note the issue's hint pointed at a null element ref. The actual null is the synthetic event's currentTarget — there is no ref.current.checked anywhere in app/src.

Solution

Capture the value synchronously, then close the updater over it:

onChange={event => {
  const { checked } = event.currentTarget;
  setClearMemoryOnDisconnect(prev => ({ ...prev, [compositeKey]: checked }));
}}

Applied at all three affected sites: TelegramConfig, and both of DiscordConfig's (the managed_dm branch and the all-other-modes branch). The functional-updater form is retained — it is correct for this keyed map — only the event read moves out of it.

Coverage of the bug class. All 35 currentTarget reads in app/src were reviewed; the other 32 are synchronous reads inside their handler and are unaffected. ComposioConnectModal renders the same checkbox but passes a plain value rather than an updater, so it was never at risk.

analyticsInteractions.ts. An earlier attempt at this issue wrapped element.checked in a try/catch, commented as guarding a disconnected DOM node whose checked access "throws". Verified against jsdom that HTMLInputElement's checked getter returns normally on both disconnected and never-mounted nodes and cannot throw — the catch was unreachable, and its comment misdiagnosed this issue for the next reader. Replaced with a direct return and an accurate note. No behaviour change.

Submission Checklist

If a section does not apply to this change, mark the item as N/A with a one-line reason. Do not delete items.

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80% — changed lines (Vitest + cargo-llvm-cov merged via diff-cover) meet the gate enforced by .github/workflows/ci-lite.yml. Run pnpm test:coverage and pnpm test:rust locally; PRs below 80% on changed lines will not merge.
  • N/A: bug fix only — no feature rows added, removed, or renamed. Coverage matrix updated — added/removed/renamed feature rows in docs/TEST-COVERAGE-MATRIX.md reflect this change
  • N/A: no matrix rows change, so no feature IDs apply. All affected feature IDs from the matrix are listed in the PR description under ## Related
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • N/A: no release-cut surface touched — an existing checkbox keeps its existing behaviour. Manual smoke checklist updated if this touches release-cut surfaces (docs/RELEASE-MANUAL-SMOKE.md)
  • Linked issue closed via Closes #NNN in the ## Related section

Regression tests

Three cases, one per patched render branch, in TelegramConfig.test.tsx and DiscordConfig.test.tsx. Each fires three toggles inside one act batch: the first takes React's eager-state path, the rest find pending lanes and defer their updater to render time — precisely the window where currentTarget is null.

Each then asserts an odd toggle count leaves the box checked and that disconnect carries clearMemory: true, so the test proves the deferred updates actually applied rather than merely not throwing.

Verified failing before the fix by reverting each site in turn, with the exact production error:

  • Telegram reverted → 1 failure, TypeError: Cannot read properties of null (reading 'checked')
  • Discord reverted → 2 failures, same error (one per branch)

Impact

  • Platform: desktop (Tauri/CEF) React renderer only. No Rust, no core, no RPC, no schema change.
  • User-visible: toggling "also delete memory" on the Telegram/Discord disconnect flow no longer throws; the setting now applies reliably instead of intermittently failing mid-update.
  • Performance / security / migration / compatibility: none.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Keep this section for AI-authored PRs. For human-only PRs, mark each field N/A.

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: N/A
  • Commit SHA: N/A

Validation Run

  • pnpm --filter openhuman-app format:check — Prettier clean on all five changed files
  • pnpm typecheck — clean
  • Focused tests: channels + services suites 114 files / 1494 tests passed; full frontend suite 768 files / 8972 tests passed, 0 failed
  • N/A: no Rust changed. Rust fmt/check (if changed)
  • N/A: no Tauri shell changed. Tauri fmt/check (if changed)

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: none beyond removing the crash. The checkbox drives the same clearMemory disconnect flag it always did.
  • User-visible effect: the toggle stops throwing and applies reliably.

Parity Contract

  • Legacy behavior preserved: yes. The functional-updater form is retained (correct for this keyed map); only the event read moves out of it, and analyticsInteractions.controlState returns exactly the same values as before.
  • Guard/fallback/dispatch parity checks: controlState's checkbox/radio, range, select, and aria-checked branches are unchanged; the removed catch was unreachable, so no fallback path was lost.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none
  • Canonical PR: this PR
  • Resolution (closed/superseded/updated): N/A

…inyhumansai#5161)

The "also delete memory" checkbox in the Telegram and Discord config
panels read `event.currentTarget.checked` from inside a functional
setState updater:

    onChange={event =>
      setClearMemoryOnDisconnect(prev => ({
        ...prev,
        [compositeKey]: event.currentTarget.checked,
      }))
    }

React resets `currentTarget` to null as soon as the handler returns, and
a functional updater is not evaluated in the handler — React invokes it
later while processing the update queue. The read therefore hit a nulled
`currentTarget` and threw `TypeError: Cannot read properties of null
(reading 'checked')`.

It only failed sometimes because React evaluates an updater eagerly while
the fiber has no pending work, which keeps the read inside the handler's
synchronous window. Once any other update was already queued — a second
toggle, a concurrent render — the eager path is skipped and the updater
ran against the nulled event. That matches the low, spread-out event
count in Sentry (TAURI-REACT-39) and the `useState` frame in its stack.

Read `checked` synchronously in the handler and close the updater over
the captured value. Three sites: Telegram's checkbox and both of
Discord's (the managed_dm branch and the all-other-modes branch).
`ComposioConnectModal` has the same checkbox but passes a plain value, so
it was never affected; a sweep of all 35 `currentTarget` reads in
`app/src` confirms no other deferred read remains.

Also drop the unreachable try/catch in `analyticsInteractions.ts`, which
attributed this issue to a disconnected DOM node. `HTMLInputElement`'s
`checked` getter works on disconnected and never-mounted nodes and cannot
throw, so the guard could never fire and its comment misdiagnosed the
bug for the next reader.

Regression tests fire three toggles in one batch so the first takes the
eager path and the rest defer, then assert the checkbox is still checked
and the disconnect call carries `clearMemory: true`. Verified failing
before the fix with the exact production error, one case per patched
render branch.
@M3gA-Mind
M3gA-Mind requested a review from a team July 30, 2026 13:27
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0b5d954f-0acb-4a95-b070-aec7a5fb4df4

📥 Commits

Reviewing files that changed from the base of the PR and between bb83836 and 7cdef28.

📒 Files selected for processing (5)
  • app/src/components/channels/DiscordConfig.tsx
  • app/src/components/channels/TelegramConfig.tsx
  • app/src/components/channels/__tests__/DiscordConfig.test.tsx
  • app/src/components/channels/__tests__/TelegramConfig.test.tsx
  • app/src/services/analyticsInteractions.ts

Comment @coderabbitai help to get the list of available commands.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

M3gA-Mind has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@M3gA-Mind
M3gA-Mind merged commit 19efd7a into tinyhumansai:main Jul 31, 2026
23 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TypeError: Cannot read properties of null (reading 'checked') — checkbox input ref

1 participant