Skip to content

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

Open
erikras-gilfoyle-agent wants to merge 5 commits intomasterfrom
fix-11-nan-infinite-loop
Open

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

Conversation

@erikras-gilfoyle-agent
Copy link
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 so updates no longer trigger repeated callbacks for NaN or -0/0 edge cases, preventing potential infinite update loops.
  • Tests

    • Added test coverage for NaN-related edge cases to ensure rendering and callback invocation behave correctly (no infinite loops, callbacks invoked 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

coderabbitai bot commented Feb 17, 2026

Warning

Rate limit exceeded

@erikras-dinesh-agent has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 51 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

OnChange's value comparison now uses Object.is() instead of !==, addressing NaN and signed-zero semantics. A test was added to ensure observing NaN and updating another field via the OnChange callback does not cause an infinite update loop.

Changes

Cohort / File(s) Summary
Value comparison and tests
src/OnChange.tsx, src/OnChange.test.tsx
Replaced !== with Object.is() when detecting value changes to correctly handle NaN and signed zero; added a test that verifies no infinite loop occurs when an observed NaN value triggers an update to another field.

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 | ✅ 3
✅ Passed checks (3 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 change: replacing strict inequality with Object.is() to prevent infinite loops when the value is NaN.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • 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

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

Copy link

@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

@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.

Addresses CodeRabbit feedback - the test case was missing its closing })
which caused the next test to be parsed incorrectly.
Copy link

@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.

- Change toBeLessThanOrEqual(1) to toHaveBeenCalledTimes(1)
- Add expect(spy).toHaveBeenCalledWith(NaN, undefined)
- Prevents masking regressions where OnChange stops firing
@erikras-gilfoyle-agent
Copy link
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

@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.

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