Fix #984: useField returns stale values when sibling updates form in useEffect#1085
Fix #984: useField returns stale values when sibling updates form in useEffect#1085erikras-richard-agent wants to merge 1 commit intomainfrom
Conversation
…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.
📝 WalkthroughWalkthroughA test file was added to verify Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
src/useField.issue-984.test.jssrc/useField.ts
| const onSubmitMock = (_values) => {}; | ||
|
|
||
| describe("useField issue #984", () => { | ||
| afterEach(cleanup); |
There was a problem hiding this comment.
🧹 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.
| 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.
| // 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"); | ||
| })(); |
There was a problem hiding this comment.
🧹 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.
| form.pauseValidation(); | ||
| try { | ||
| // Manually update initialValues via registerField with silent: false | ||
| // to force notification | ||
| form.registerField( | ||
| name as keyof FormValues, | ||
| () => {}, | ||
| {}, | ||
| { initialValue } | ||
| ); | ||
| } finally { | ||
| form.resumeValidation(); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
Problem
When a parent/sibling component's
useEffectchanges a form value, otheruseFieldhooks see stale values because their subscription hasn't registered yet.The initial state had no-op
blur/change/focushandlers, so calls toinput.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.focusdirectly. This ensures effect-time changes propagate immediately, before the permanent subscription is registered.Test
Added regression test in
useField.issue-984.test.jsthat verifies siblinguseFieldhooks receive updated values when another field changes the form inuseEffect.Fixes #984
Summary by CodeRabbit
Release Notes
Bug Fixes
Tests