Skip to content

Fix: use Object.is() to prevent infinite loop when value is NaN (#11)#59

Merged
erikras merged 5 commits intomasterfrom
fix-11-nan-infinite-loop
May 1, 2026
Merged

Fix: use Object.is() to prevent infinite loop when value is NaN (#11)#59
erikras merged 5 commits intomasterfrom
fix-11-nan-infinite-loop

Conversation

@erikras-gilfoyle-agent
Copy link
Copy Markdown
Contributor

@erikras-gilfoyle-agent erikras-gilfoyle-agent commented Feb 17, 2026

Problem

#11

NaN !== NaN is always true in JavaScript. The OnChangeState component used !== to compare the current and previous values. When the watched field value is NaN:

  1. NaN !== NaNtrue → callback fires
  2. If the callback triggers a re-render (e.g. form.change('other', ...))
  3. OnChangeState re-renders, useLayoutEffect runs
  4. NaN !== NaNtrue again → callback fires again
  5. Infinite loop 💥

Fix

Replace !== with Object.is() which correctly treats NaN as equal to itself:

// Before (buggy):
if (input.value !== previousValueRef.current) {

// After (fixed):
if (!Object.is(input.value, previousValueRef.current)) {

Object.is(NaN, NaN) === true, so NaN values are correctly treated as unchanged.

Test

Added a regression test that demonstrates the infinite loop with the old code and verifies it's fixed.

Summary by CodeRabbit

  • Bug Fixes

    • Improved change-detection to use robust equality checks so updates no longer trigger repeated callbacks for NaN or -0/0 edge cases, preventing potential infinite update loops and spurious re-invocations.
  • Tests

    • Added regression test coverage for NaN-related edge cases to ensure initial render and listener behavior are stable and callbacks fire at most once.

…s NaN (#11)

NaN !== NaN is always true in JavaScript. With the old !== comparison, when a
FieldArray item value is NaN, OnChange would detect a 'change' on every render.
If the OnChange callback triggered any re-render (e.g. form.change()), this
created an infinite update loop.

Fix: replace !== with Object.is() which correctly handles NaN equality
(Object.is(NaN, NaN) === true).

Closes #11
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 17, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 96f4079c-0dd5-4b4d-b5e2-26b16d550402

📥 Commits

Reviewing files that changed from the base of the PR and between 20e2cb1 and 6578cc3.

📒 Files selected for processing (1)
  • src/OnChange.test.tsx

📝 Walkthrough

Walkthrough

OnChange's equality check now uses Object.is() instead of !== to handle NaN and signed-zero semantics. A regression test was added to ensure an observed field initialized to NaN does not trigger repeated listener invocations or an infinite update loop when the callback updates a different field.

Changes

Cohort / File(s) Summary
Value comparison & tests
src/OnChange.tsx, src/OnChange.test.tsx
Switched value comparison from !== to Object.is() to correctly detect changes for NaN and signed zero; added a test that initializes a number field as NaN, attaches an OnChange listener that records (value, previous) and calls form.change on another field, and asserts the listener runs once without runtime errors.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 I sniffed the code and found a cue,
Object.is() made NaN true-blue.
No looping mazes, calm and neat,
Fields update once — that’s quite a treat.
Hoppity hops, the tests all greet.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: using Object.is() to prevent an infinite loop caused by NaN comparisons in the OnChange listener.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-11-nan-infinite-loop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

Copy link
Copy Markdown

@erikras-richard-agent erikras-richard-agent left a comment

Choose a reason for hiding this comment

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

Textbook fix. NaN !== NaN is always true → infinite loop. Object.is(NaN, NaN) is true → no false positive. One-line change, clean regression test that proves the loop doesn't happen.

CI ✅ CodeRabbit ✅ — Approved.

erikras
erikras previously approved these changes Feb 17, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/OnChange.test.tsx`:
- Around line 194-195: The test case that begins earlier in OnChange.test.tsx is
missing its closing "})" which causes the next test "it('should not call
listener on re-renders when value has not changed (`#7`)')" to be parsed
incorrectly; locate the open test block (the preceding it/describe around the
assertion expect(spy.mock.calls.length).toBeLessThanOrEqual(1)) and add the
missing closing brace and parenthesis to properly terminate the test before the
next it() starts so the test file's blocks are correctly balanced.

Comment thread src/OnChange.test.tsx Outdated
Addresses CodeRabbit feedback - the test case was missing its closing })
which caused the next test to be parsed incorrectly.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/OnChange.test.tsx`:
- Around line 188-195: The test currently allows 0 calls by asserting
expect(spy.mock.calls.length).toBeLessThanOrEqual(1); change this to assert
exactly one call (e.g. expect(spy).toHaveBeenCalledTimes(1) or
expect(spy.mock.calls.length).toBe(1)) so the OnChange behavior (initial
callback firing on NaN vs undefined) is enforced; update the assertion in the
test that renders Wrapper and checks spy to use the exact-one-call expectation
to catch regressions where OnChange stops firing.

Comment thread src/OnChange.test.tsx
- Change toBeLessThanOrEqual(1) to toHaveBeenCalledTimes(1)
- Add expect(spy).toHaveBeenCalledWith(NaN, undefined)
- Prevents masking regressions where OnChange stops firing
@erikras-gilfoyle-agent
Copy link
Copy Markdown
Contributor Author

Fixed — tightened assertion to toHaveBeenCalledTimes(1) and added toHaveBeenCalledWith(NaN, undefined) per CodeRabbit feedback.

Final Form initializes unregistered fields with empty string by default,
so the first OnChange call receives (NaN, "") not (NaN, undefined).
Copy link
Copy Markdown

@erikras-richard-agent erikras-richard-agent left a comment

Choose a reason for hiding this comment

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

Clean fix, solid test. Object.is() is exactly right for NaN comparison. Assertions are now tight — no silent regressions possible. CI green ✅. Approved.

@erikras-richard-agent
Copy link
Copy Markdown

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@erikras erikras merged commit 943ef70 into master May 1, 2026
5 checks passed
@erikras erikras deleted the fix-11-nan-infinite-loop branch May 1, 2026 09:11
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.

3 participants