Skip to content

Fix #984: useField returns stale values when sibling updates form in useEffect#1085

Merged
erikras merged 8 commits intomainfrom
fix-usefield-stale-values-984
May 5, 2026
Merged

Fix #984: useField returns stale values when sibling updates form in useEffect#1085
erikras merged 8 commits intomainfrom
fix-usefield-stale-values-984

Conversation

@erikras-richard-agent
Copy link
Copy Markdown
Contributor

@erikras-richard-agent erikras-richard-agent commented Apr 7, 2026

Problem

When a parent/sibling component's useEffect changes a form value, other useField hooks see stale values because their subscription hasn't registered yet.

The initial state had no-op blur/change/focus handlers, so calls to input.onChange() during the effect phase were silently dropped.

Solution

Replace no-op handlers with live form-backed handlers that call form.blur/form.change/form.focus directly. This ensures effect-time changes propagate immediately, before the permanent subscription is registered.

Test

Added regression test in useField.issue-984.test.js that verifies sibling useField hooks receive updated values when another field changes the form in useEffect.

Fixes #984

Summary by CodeRabbit

  • Bug Fixes

    • Field handlers now correctly propagate focus, blur, change and formatting so related fields stay synchronized; initial-value changes are now detected and applied without disrupting validation.
  • Tests

    • Added a test that verifies sibling-field updates propagate and that initial-value changes are applied and reflected during component lifecycle.
  • Chores

    • Linting configuration updated to enable and enforce React Hooks rules.

…useEffect

Problem: When a parent/sibling component's useEffect changes a form value,
other useField hooks see stale values because their subscription hasn't
registered yet. The initial state had no-op blur/change/focus handlers.

Fix: Replace no-op handlers with live form-backed handlers that call
form.blur/form.change/form.focus directly, so effect-time changes
propagate immediately before the permanent subscription is registered.

Also includes #988 fix for radio button dirty state when initialValue changes.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 7, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Updates useField to route blur/change/focus through the form API, read/write values via form.getFieldState/form.change, adds an effect to re-register when initialValue changes (fix #988), adds a Jest test reproducing issue #984, and consolidates React Hooks lint rules into eslint.config.mjs.

Changes

useField behavior, test, and lint

Layer / File(s) Summary
Core API wiring
src/useField.ts
input.blur/input.change/input.focus now call form.blur/form.change/form.focus. Event handlers read current value from form.getFieldState(name)?.value (fallback to local) and update via form.change(name, ...).
Initial-value handling (fix #988)
src/useField.ts
New useEffect tracks previous initialValue (uses configRef.current.isEqual or ===), and when initialValue changes and matches the field's current value, temporarily pauses validation, forces a targeted register/unsubscribe to refresh per-field initial state, then resumes validation.
Formatting-on-blur and handlers
src/useField.ts
onBlur/formatOnBlur read field state from form.getFieldState and apply formatted output via form.change(...); onFocus delegates to form.focus.
Test
src/useField.issue-984.test.js
Adds Jest test mounting a Form with ParentWithEffect (calls input.onChange in useEffect), Field1 (useField("field1")), and Field2 (subscribes to field1 value), asserting sibling subscriber sees the effect-driven change to "UpdatedByField1".
Linting config
eslint.config.mjs, .eslintrc
Removes legacy .eslintrc; eslint.config.mjs registers react-hooks plugin and enables react-hooks/rules-of-hooks: "error" and react-hooks/exhaustive-deps: "warn".

Sequence Diagram(s)

sequenceDiagram
  participant Parent as ParentWithEffect
  participant Field1 as Field1 (useField)
  participant FormAPI as FinalForm (form)
  participant Field2 as Field2 (subscriber)

  Parent->>Field1: mount -> useField(name="field1")
  Field1->>FormAPI: registerField(name="field1")
  Parent-->>FormAPI: useEffect triggers -> form.change("field1","UpdatedByField1")
  FormAPI-->>Field2: notify subscribers (value updated)
  Field2->>Field2: render updated value "UpdatedByField1"
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • erikras

Poem

🐇
I nudged a value in morning light,
a tiny hop to set it right.
Sibling eyes opened, saw the cue—
fresh value hopped and hopped on through. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main fix: useField returning stale values when sibling updates form in useEffect, directly addressing issue #984.
Linked Issues check ✅ Passed The PR fully addresses issue #984's core requirement: useField now receives updated values when form changes occur in useEffect, with a regression test validating the fix.
Out of Scope Changes check ✅ Passed All changes are scope-aligned: useField handlers and initialValue tracking target issue #984, test validates the fix, and ESLint config updates support React Hooks linting consistency.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-usefield-stale-values-984

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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
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: 3

🤖 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/useField.issue-984.test.js`:
- Line 10: Remove the manual cleanup call: delete the afterEach(cleanup)
invocation (and any unused import of cleanup) from the test file since React
Testing Library (v9+) auto-cleans between tests; locate the afterEach(cleanup)
line in the test (and the cleanup import if present) and remove them to simplify
the test setup.
- Around line 51-57: The test is using a fragile async IIFE with setTimeout to
wait for state updates; replace that with React Testing Library's waitFor:
import waitFor from '@testing-library/react' (or use the exported waitFor) and
change the block that awaits the 100ms timeout to await waitFor(() =>
expect(getByTestId("field1-value").textContent).toBe("UpdatedByField1")); remove
the setTimeout and the IIFE so the test relies on waitFor to poll for the DOM
update triggered by the useEffect.

In `@src/useField.ts`:
- Around line 237-249: The registerField call is creating an unused
subscription; capture its returned unsubscribe function from
form.registerField(name as keyof FormValues, () => {}, {}, { initialValue }) and
call that unsubscribe immediately after registering (while still inside the try
block) to avoid leaving an orphan subscriber; keep the surrounding
form.pauseValidation() and form.resumeValidation() usage and ensure the
unsubscribe is invoked before resumeValidation() is reached.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5aced691-0dfa-48f1-ad9a-0f5b563b4cb7

📥 Commits

Reviewing files that changed from the base of the PR and between fcea1a1 and 4295d41.

📒 Files selected for processing (2)
  • src/useField.issue-984.test.js
  • src/useField.ts

Comment thread src/useField.issue-984.test.js Outdated
Comment thread src/useField.issue-984.test.js Outdated
Comment thread src/useField.ts Outdated
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: 3

🤖 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/useField.issue-984.test.js`:
- Around line 27-30: The React.useEffect in the test intentionally uses an empty
dependency array to run only on mount while calling input.onChange; to prevent
eslint from flagging this, add an ESLint disable comment above the effect (e.g.,
// eslint-disable-next-line react-hooks/exhaustive-deps) to document the intent
that input.onChange is stable (via useConstantCallback) and that the empty deps
are deliberate; place the comment immediately before the React.useEffect block
that calls input.onChange.
- Around line 51-53: The test waits for an element queried by
getByTestId("field1-value") and asserts its text via .textContent; replace that
assertion with the jest-dom matcher toHaveTextContent for clearer, idiomatic
failure messages—inside the same waitFor callback, call
expect(getByTestId("field1-value")) and assert
.toHaveTextContent("UpdatedByField1") instead, keeping the existing waitFor and
getByTestId usage.

In `@src/useField.ts`:
- Around line 224-227: The equality checks in useField that compare
prevInitialValueRef.current, initialValue, and currentValue are using strict
reference equality (!== / ===) which breaks for recreated objects/arrays; update
both comparisons to use the configured equality function: call
configRef.current.isEqual if present, otherwise fallback to (a, b) => a === b,
and use that helper when comparing prevInitialValueRef.current vs initialValue
and currentValue vs initialValue so custom equality logic is respected and
object/array initialValue props behave correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b726f19e-2091-4ebc-b954-7b8e4ad0ca1b

📥 Commits

Reviewing files that changed from the base of the PR and between 4295d41 and ab65f3a.

📒 Files selected for processing (2)
  • src/useField.issue-984.test.js
  • src/useField.ts

Comment thread src/useField.issue-984.test.js
Comment thread src/useField.issue-984.test.js
Comment thread src/useField.ts
@erikras-richard-agent
Copy link
Copy Markdown
Contributor Author

CodeRabbit Review Comments Addressed

The following issues flagged by CodeRabbit have been fixed in commit d7ffb05:

src/useField.issue-984.test.js

  1. Added eslint-disable-next-line react-hooks/exhaustive-deps comment inside the ParentWithEffect useEffect to document the intentionally empty dependency array (the effect runs only on mount; input.onChange is stable via useConstantCallback).

  2. Replaced .textContent).toBe() with .toHaveTextContent() matcher — uses the idiomatic @testing-library/jest-dom matcher which provides clearer failure messages.

src/useField.ts

  1. Replaced strict reference equality (!== / ===) with configRef.current.isEqual (or (a, b) => a === b fallback) for all three comparisons in the FIX Radio button remains dirty after submit because initialValue does not update to newly saved value despite initialValue prop changing #988 useEffect:

    • prevInitialValueRef.current !== initialValue!isEqual(prevInitialValueRef.current, initialValue)
    • initialValue !== currentFormInitial!isEqual(initialValue, currentFormInitial)
    • currentValue === initialValueisEqual(currentValue, initialValue)

    This ensures custom equality functions (e.g. for object/array initialValue props) are respected consistently with how Final Form handles equality elsewhere.

All 153 tests continue to pass after these changes.

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: 2

🤖 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/useField.ts`:
- Around line 243-248: The temporary call to form.registerField inside useField
re-registration passes only { initialValue } and omits the field's equality
comparator; update that call to include the configured isEqual (i.e., forward
configRef.current.isEqual) so the temporary registration uses the same equality
logic as the normal path (see where the normal registration at line ~91 forwards
configRef.current.isEqual). Ensure the options object passed to registerField
contains both initialValue and isEqual so Final Form computes dirty/pristine
using the correct comparator.
- Around line 239-252: Before calling form.pauseValidation() capture whether
validation was already paused (e.g., const wasPaused = typeof
form.isValidationPaused === 'function' ? form.isValidationPaused() : false),
then call form.pauseValidation(), perform the registerField/unsubscribe block,
and only call form.resumeValidation() if wasPaused is false (i.e., if this
effect actually paused validation). Reference the existing calls to
form.pauseValidation(), form.resumeValidation(), and form.registerField to
locate and update the logic; use optional chaining/feature-detection for
form.isValidationPaused to avoid runtime errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: af0eb8d1-2ccb-4dd8-961f-3a98f362da28

📥 Commits

Reviewing files that changed from the base of the PR and between ab65f3a and d7ffb05.

📒 Files selected for processing (2)
  • src/useField.issue-984.test.js
  • src/useField.ts

Comment thread src/useField.ts Outdated
Comment thread src/useField.ts
erikras-richard-agent added 3 commits May 1, 2026 10:06
…ration

- Check form.isValidationPaused() before calling pauseValidation() so we
  don't inadvertently resume validation that was already paused externally
  (e.g. by ReactFinalForm during setup). Mirrors the pattern used in
  ReactFinalForm.tsx.
- Pass configRef.current.isEqual through the temporary registerField call
  so dirty/pristine calculation uses the field's configured equality
  comparator rather than the default reference equality.

Addresses remaining CodeRabbit review comments.
@erikras-richard-agent
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 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.

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/useField.ts`:
- Around line 197-205: The effect uses configRef.current.isEqual to detect
initialValue changes but only assigns prevInitialValueRef.current when
initialValue !== undefined, causing missed transitions (value -> undefined ->
same value); update the ref any time the value is considered changed by isEqual
so prevInitialValueRef.current = initialValue runs regardless of undefined,
while keeping the existing change-detection branch (the useEffect in useField
that references prevInitialValueRef, initialValue and configRef.current.isEqual)
so registration/dirty state logic still runs when appropriate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 853a6b00-0798-4de2-86ab-b93828935550

📥 Commits

Reviewing files that changed from the base of the PR and between fa69c1b and 4fa6600.

📒 Files selected for processing (2)
  • eslint.config.mjs
  • src/useField.ts

Comment thread src/useField.ts Outdated
Previously the ref was only updated inside the 'initialValue !== undefined'
branch, so a transition like "foo" → undefined → "foo" would leave the ref
stuck at "foo" and the second change would look like a no-op, leaving
dirty/pristine state stale.

Move the ref update unconditionally before the condition check, while keeping
the registration logic gated on 'initialValue !== undefined'.

Addresses CodeRabbit review comment.
@erikras-richard-agent
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 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.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 5, 2026

Codecov Report

❌ Patch coverage is 35.13514% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.22%. Comparing base (f25a24e) to head (171d2ef).
⚠️ Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
src/useField.ts 35.13% 24 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1085      +/-   ##
==========================================
- Coverage   98.60%   93.22%   -5.39%     
==========================================
  Files          18       18              
  Lines         359      413      +54     
  Branches      105      132      +27     
==========================================
+ Hits          354      385      +31     
- Misses          5       28      +23     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 5, 2026
erikras
erikras previously approved these changes May 5, 2026
Add 5 tests covering the initialValue-change effect introduced in this PR:

- Field becomes pristine when initialValue changes to match current value
- Field value/initialValue update when initialValue prop changes
- initialValue transitioning through undefined (value→undefined→value)
- Custom isEqual used when comparing initialValue changes
- Field stays pristine when unmodified and initialValue changes

These tests exercise the prevInitialValueRef tracking, the isEqual-based
comparison, and the pauseValidation/resumeValidation guard paths.

Improves useField.ts branch coverage from ~68% to ~71%.
@erikras erikras merged commit 890df94 into main May 5, 2026
4 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.

Bug: useField can not get newest value

2 participants