Skip to content

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

Open
erikras-richard-agent wants to merge 1 commit intomainfrom
fix-usefield-stale-values-984
Open

Fix #984: useField returns stale values when sibling updates form in useEffect#1085
erikras-richard-agent wants to merge 1 commit 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

Release Notes

  • Bug Fixes

    • Enhanced form field event handling to properly integrate with form state management, ensuring field updates are correctly propagated across sibling components and initial values synchronize appropriately.
  • Tests

    • Added test coverage verifying form field behavior consistency when parent components update field values during component lifecycle.

…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

📝 Walkthrough

Walkthrough

A test file was added to verify useField behavior when parent components update form values during useEffect, and useField.ts was modified to wire blur/focus/change handlers to Final Form APIs and track initialValue prop changes by re-registering fields with updated initial values.

Changes

Cohort / File(s) Summary
Test Coverage
src/useField.issue-984.test.js
New test verifying that sibling field components receive updated form values when a parent component modifies form state within a mount-only useEffect.
useField Hook Implementation
src/useField.ts
Updated to wire blur, focus, and change handlers to Final Form APIs; derives field value from form.getFieldState(name)?.value before parsing; calls form.change() for onChange and formatOnBlur operations. Added effect to detect initialValue prop changes and re-register fields with updated initial values via form.registerField() while temporarily pausing validation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • erikras

Poem

🐰 A field that wouldn't play along,
Till useField learned the form's sweet song.
Now handlers dance with Final's grace,
And initialValues find their place!
—The Code Rabbit 🌿✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Out of Scope Changes check ❓ Inconclusive The PR includes changes to useField.ts for the main fix and adds a regression test file. However, the PR objectives mention an additional fix for issue #988 (radio button dirty state) which is not listed in the linked issues. Clarify whether the radio button dirty state fix (#988) is in scope or should be separated into a different PR for clarity.
✅ 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: fixing stale values in useField when sibling components update the form in useEffect.
Linked Issues check ✅ Passed The PR addresses issue #984 by replacing no-op handlers with form-backed handlers (form.blur, form.change, form.focus) and adding a regression test, directly solving the reported stale value issue.

✏️ 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-usefield-stale-values-984

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

const onSubmitMock = (_values) => {};

describe("useField issue #984", () => {
afterEach(cleanup);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Optional: Manual cleanup is redundant with modern React Testing Library.

Starting from @testing-library/react v9+, cleanup is called automatically after each test when using a framework that supports afterEach (like Jest). The manual afterEach(cleanup) can be removed.

♻️ Proposed simplification
 describe("useField issue `#984`", () => {
-  afterEach(cleanup);
-
   // https://github.com/final-form/react-final-form/issues/984
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
afterEach(cleanup);
describe("useField issue `#984`", () => {
// https://github.com/final-form/react-final-form/issues/984
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/useField.issue-984.test.js` at 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.

Comment on lines +51 to +57
// After useEffect runs, Field2 should see the updated value
// This is the bug: Field2 sees stale "InitialField1" instead
await (async () => {
// Wait a bit for effects to settle
await new Promise((resolve) => setTimeout(resolve, 100));
expect(getByTestId("field1-value").textContent).toBe("UpdatedByField1");
})();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Replace setTimeout with waitFor for more reliable testing.

The current approach using setTimeout(100ms) with an unnecessary async IIFE is both fragile and overly complex. React Testing Library's waitFor is the idiomatic approach for waiting on asynchronous state updates.

♻️ Proposed refactor using waitFor
+import { render, cleanup, waitFor } from "@testing-library/react";
-import { render, cleanup } from "@testing-library/react";
     // After useEffect runs, Field2 should see the updated value
     // This is the bug: Field2 sees stale "InitialField1" instead
-    await (async () => {
-      // Wait a bit for effects to settle
-      await new Promise((resolve) => setTimeout(resolve, 100));
-      expect(getByTestId("field1-value").textContent).toBe("UpdatedByField1");
-    })();
+    await waitFor(() => {
+      expect(getByTestId("field1-value")).toHaveTextContent("UpdatedByField1");
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/useField.issue-984.test.js` around lines 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.

Comment on lines +237 to +249
form.pauseValidation();
try {
// Manually update initialValues via registerField with silent: false
// to force notification
form.registerField(
name as keyof FormValues,
() => {},
{},
{ initialValue }
);
} finally {
form.resumeValidation();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential subscription leak: registerField return value not used.

The form.registerField() call at line 241 returns an unsubscribe function that is never called. This creates an orphan subscription with an empty callback. While the callback and subscription are empty, this still consumes memory in Final Form's internal subscriber list.

Consider storing and immediately calling the unsubscribe function:

🔧 Proposed fix to prevent subscription leak
             form.pauseValidation();
             try {
               // Manually update initialValues via registerField with silent: false
               // to force notification
-              form.registerField(
+              const unsubscribe = form.registerField(
                 name as keyof FormValues,
                 () => {},
                 {},
                 { initialValue }
               );
+              unsubscribe();
             } finally {
               form.resumeValidation();
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
form.pauseValidation();
try {
// Manually update initialValues via registerField with silent: false
// to force notification
form.registerField(
name as keyof FormValues,
() => {},
{},
{ initialValue }
);
} finally {
form.resumeValidation();
}
form.pauseValidation();
try {
// Manually update initialValues via registerField with silent: false
// to force notification
const unsubscribe = form.registerField(
name as keyof FormValues,
() => {},
{},
{ initialValue }
);
unsubscribe();
} finally {
form.resumeValidation();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/useField.ts` around lines 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.

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

1 participant