Skip to content

fix(useField): Fix no re-render on initialValue change#1034

Closed
jspannu919 wants to merge 1 commit intofinal-form:mainfrom
jspannu919:fix_no_render
Closed

fix(useField): Fix no re-render on initialValue change#1034
jspannu919 wants to merge 1 commit intofinal-form:mainfrom
jspannu919:fix_no_render

Conversation

@jspannu919
Copy link

@jspannu919 jspannu919 commented May 20, 2023

Currently useField doesn't retriggers properly if the initialValue is changed (Added test scenario).
Added explicit check for initial value change to prevent deepEqual overheads.

Summary by CodeRabbit

  • Bug Fixes

    • Improved initial value handling for form fields to ensure values are correctly reflected on first render and subsequent updates.
  • Tests

    • Added test coverage for initial value behavior in form fields.

@codesandbox-ci
Copy link

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit fe2fb36:

Sandbox Source
final-form/react-final-form Configuration
final-form/react-final-form Configuration
final-form/react-final-form Configuration
final-form/react-final-form Configuration
final-form/react-final-form Configuration

@bslipek
Copy link

bslipek commented May 25, 2023

IMO this is fine, you should not listen for initialValues after form initialization. Why you need this?

@jspannu919
Copy link
Author

@bslipek In data driven forms (Ref: https://github.com/data-driven-forms/react-forms ) for conditionally hiding the fields, the fields are wrapped with a Field component so that they get re-rendered when the value in any of the parent attribute change(Ref: https://github.com/data-driven-forms/react-forms/blob/84c4cb8861d689cb87ba68100fe7d3acfd667655/packages/react-form-renderer/src/form-renderer/render-form.js#L47). So, due to the above issue the conditional visibility doesn't works if we reverse the order of the fields(Ref: https://stackblitz.com/edit/1dkk8s-rd1ejn?file=schema.js).

@bslipek
Copy link

bslipek commented May 26, 2023

I got your point. But still think that this is not final-form problem. But I am just a observer here :)

@EndMove
Copy link

EndMove commented Apr 14, 2025

Everything is in the name of the props "initialValue"

@erikras-richard-agent
Copy link
Contributor

Thanks for this PR! There's an interesting design question here.

The issue: useField doesn't re-render when initialValue prop changes after mount.

Community perspective (from comments):
@bslipek and @EndMove suggest initialValue is intentionally for initial values only, not dynamic updates. If you need dynamic values, you should update form state directly rather than changing initialValue.

Code concerns:

  • Uses != instead of !== for comparison
  • Logic seems backwards: setState only on initial render if initial changed?
  • The test doesn't actually verify the behavior works

Design question for @erikras:
Should initialValue be reactive (re-initialize when it changes), or is it intentionally only for initial mount?

  • If reactive: This needs better implementation
  • If not reactive: Close this PR and document that initialValue is for initialization only

My instinct: The name "initialValue" suggests it's for initialization, not dynamic updates. For dynamic defaults, use form-level initialValues or form.change().

What's your intent here? 🤔

Copy link
Contributor

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

Review by Richard (Team Lead)

This addresses a real bug: when initialValue changes on first render, the field state doesn't update because the first render callback is skipped entirely.

Issues:

  1. Missing space: if(state.initial != newState.initial) — needs space after if and should use !== (strict equality)
  2. Shallow comparison: state.initial != newState.initial won't work for object/array initial values. Should use deep equality or at minimum JSON.stringify comparison.
  3. Test is incomplete: The test name is should listen to initial value 2 (not descriptive), the expect(input.value).toBe('test') is commented out, and there's no assertion that the value actually propagates correctly.
  4. Variable shadowing: The outer state from the closure and newState parameter could be confusing. The original code reused state as the param name intentionally.

Verdict: Right direction but needs polish. The bug is real — would be better to fix this properly in our codebase with strict equality and proper test coverage.

@erikras-richard-agent
Copy link
Contributor

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 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.

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

📝 Walkthrough

Walkthrough

This PR fixes initial value propagation in the useField hook. The change modifies how state updates are handled in a useEffect callback, ensuring initial values from Form or Field components are correctly reflected in listeners that mount after the initial Field render.

Changes

Cohort / File(s) Summary
State Management Fix
src/useField.js
Renamed state parameter to newState and restructured conditional logic to properly handle initial value updates on first render versus subsequent renders, ensuring delayed listeners receive correct initial values.
Test Coverage
src/useField.test.js
Added test case validating that useField correctly reflects initial values when a listener is rendered after Field initialization.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested reviewers

  • erikras

Poem

🐰 A hop through the fields, where values now bloom,
Initial ones fresh, dispelling the gloom,
The listener arrives, no longer too late,
For proper state updates—what a fortunate fate!

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (25 files):

⚔️ .babelrc.js (content)
⚔️ .github/workflows/ci.yml (content)
⚔️ .gitignore (content)
⚔️ README.md (content)
⚔️ examples/async-redux-submission/asyncSubmissionMiddleware.js (content)
⚔️ examples/async-redux-submission/store.js (content)
⚔️ examples/field-arrays/package.json (content)
⚔️ examples/styling-with-smooth-ui/index.js (content)
⚔️ package-scripts.js (content)
⚔️ package.json (content)
⚔️ src/Field.test.js (content)
⚔️ src/FormSpy.test.js (content)
⚔️ src/ReactFinalForm.test.js (content)
⚔️ src/renderComponent.test.js (content)
⚔️ src/useConstantCallback.test.js (content)
⚔️ src/useField.test.js (content)
⚔️ src/useForm.test.js (content)
⚔️ src/useFormState.test.js (content)
⚔️ typescript/Field.test.tsx (content)
⚔️ typescript/FormSpy.test.tsx (content)
⚔️ typescript/ReactFinalForm.test.tsx (content)
⚔️ typescript/index.d.ts (content)
⚔️ typescript/tsconfig.json (content)
⚔️ typescript/useField.test.tsx (content)
⚔️ typescript/useFormState.test.tsx (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ 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 clearly and specifically describes the main fix: addressing the issue where useField does not re-render when the initialValue prop changes after mount.
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 docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch fix_no_render
  • Post resolved changes as copyable diffs in a comment

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

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

🤖 Fix all issues with AI agents
In `@src/useField.js`:
- Around line 115-123: The register callback currently closes over state and
uses loose inequality (state.initial != newState.initial); change it to avoid
reading the captured state by using the setState functional updater and compare
newState.initial to the previous state's initial using strict !==; specifically,
inside the register((newState) => { ... }) callback, call setState(prev => { if
(firstRender.current) { firstRender.current = false; if (prev.initial !==
newState.initial) return newState; return prev; } return newState; }) so you
never reference the outer `state` variable and you use strict comparison for
`initial`.

In `@src/useField.test.js`:
- Around line 459-476: The test currently only asserts meta.initial inside
MyFieldListener on the second render, so it will pass if no re-render occurs;
update the test to explicitly fail when the re-render never happens by adding a
wait/assertion after render that checks meta.initial becomes "test" (e.g., use
waitFor or a polling expect) or have MyFieldListener flip a ref/state or call a
jest mock when it observes meta.initial === "test" and then assert that mock was
called; target the MyFieldListener component and the useField hook's
meta.initial to ensure the test fails if the re-render never occurs.

Comment on lines +115 to 123
register((newState) => {
if (firstRender.current) {
firstRender.current = false;
if(state.initial != newState.initial){
setState(newState);
}
} else {
setState(state);
setState(newState);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "useField.js" | head -20

Repository: final-form/react-final-form

Length of output: 88


🏁 Script executed:

cat -n ./src/useField.js | head -150

Repository: final-form/react-final-form

Length of output: 5439


🏁 Script executed:

cat -n ./src/useField.js | sed -n '95,140p'

Repository: final-form/react-final-form

Length of output: 1818


🏁 Script executed:

rg "type FieldState" --type js --type ts -A 10

Repository: final-form/react-final-form

Length of output: 53


🏁 Script executed:

cat -n ./src/types.js | head -100

Repository: final-form/react-final-form

Length of output: 117


🏁 Script executed:

fd "types" --type f -e ts -e js | head -20

Repository: final-form/react-final-form

Length of output: 53


🏁 Script executed:

rg "registerField" --type js --type ts -B 2 -A 5 | head -50

Repository: final-form/react-final-form

Length of output: 986


🏁 Script executed:

rg "form.registerField" -B 5 -A 15 | head -80

Repository: final-form/react-final-form

Length of output: 1205


🏁 Script executed:

rg "Object.is\|defaultIsEqual" --type js -B 2 -A 2

Repository: final-form/react-final-form

Length of output: 53


🏁 Script executed:

rg "isEqual" --type js -B 2 -A 2 | head -40

Repository: final-form/react-final-form

Length of output: 2028


🏁 Script executed:

rg "first render\|firstRender" --type js -B 3 -A 3

Repository: final-form/react-final-form

Length of output: 53


🏁 Script executed:

cat -n ./src/useField.js | sed -n '58,92p'

Repository: final-form/react-final-form

Length of output: 1482


🏁 Script executed:

rg "FieldState" --type js -A 5 | head -40

Repository: final-form/react-final-form

Length of output: 2017


🏁 Script executed:

cat -n ./src/useField.js | sed -n '113,130p'

Repository: final-form/react-final-form

Length of output: 690


Violation of stale closure pattern: avoid capturing state in the register callback, and use strict equality.

Lines 59–63 explicitly warn against using state inside register closures because it captures stale state from the current execution context. Line 118 directly violates this by reading state.initial inside the callback. Additionally, loose equality (!=) can coerce null and undefined as equal, skipping real initial value changes. Use a functional updater with strict comparison instead.

🔧 Proposed fix
      register((newState) => {
        if (firstRender.current) {
          firstRender.current = false;
-         if(state.initial != newState.initial){
-           setState(newState);
-         }
+         setState((prev) =>
+           Object.is(prev.initial, newState.initial) ? prev : newState,
+         );
        } else {
          setState(newState);
        }
      }, false),
🤖 Prompt for AI Agents
In `@src/useField.js` around lines 115 - 123, The register callback currently
closes over state and uses loose inequality (state.initial != newState.initial);
change it to avoid reading the captured state by using the setState functional
updater and compare newState.initial to the previous state's initial using
strict !==; specifically, inside the register((newState) => { ... }) callback,
call setState(prev => { if (firstRender.current) { firstRender.current = false;
if (prev.initial !== newState.initial) return newState; return prev; } return
newState; }) so you never reference the outer `state` variable and you use
strict comparison for `initial`.

Comment on lines +459 to +476
it("should listen to initial value 2", () => {
const MyFieldListener = () => {
const isFirstRender = React.useRef(true)
const { input, meta } = useField("name");
if(!isFirstRender.current){
expect(meta.initial).toBe("test");
// expect(input.value).toBe("test");
}
isFirstRender.current = false
return null;
};
render(
<Form onSubmit={onSubmitMock}>
{() => (
<form>
<MyFieldListener />
<Field name="name" component="input" data-testid="name" initialValue="test"/>
</form>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make the test fail if the re-render never happens.

The expectation runs only on the second render; if no re-render occurs, the test passes without exercising the behavior. Add an explicit wait/assertion that meta.initial becomes "test".

✅ Suggested adjustment
-import { render, fireEvent, cleanup } from "@testing-library/react";
+import { render, fireEvent, cleanup, waitFor } from "@testing-library/react";

-  it("should listen to initial value 2", () => {
+  it("should listen to initial value 2", async () => {
+    const initialSpy = jest.fn();
     const MyFieldListener = () => {
-      const isFirstRender = React.useRef(true)
-      const { input, meta } = useField("name");
-      if(!isFirstRender.current){
-        expect(meta.initial).toBe("test");
-        // expect(input.value).toBe("test");
-      }
-      isFirstRender.current = false
+      const { meta } = useField("name");
+      React.useEffect(() => {
+        initialSpy(meta.initial);
+      }, [meta.initial]);
       return null;
     };
     render(
       <Form onSubmit={onSubmitMock}>
         {() => (
           <form>
             <MyFieldListener />
             <Field name="name" component="input" data-testid="name" initialValue="test"/>
           </form>
         )}
       </Form>,
     );
+    await waitFor(() => expect(initialSpy).toHaveBeenCalledWith("test"));
   });
🤖 Prompt for AI Agents
In `@src/useField.test.js` around lines 459 - 476, The test currently only asserts
meta.initial inside MyFieldListener on the second render, so it will pass if no
re-render occurs; update the test to explicitly fail when the re-render never
happens by adding a wait/assertion after render that checks meta.initial becomes
"test" (e.g., use waitFor or a polling expect) or have MyFieldListener flip a
ref/state or call a jest mock when it observes meta.initial === "test" and then
assert that mock was called; target the MyFieldListener component and the
useField hook's meta.initial to ensure the test fails if the re-render never
occurs.

@erikras-richard-agent
Copy link
Contributor

Thank you for identifying a real issue with initialValue change detection in useField, @jspannu919! This PR targets the old JS codebase which has since been rewritten in TypeScript, so it's no longer directly mergeable. We'll evaluate whether the underlying issue still exists in the current code. Closing with appreciation. 🙏

@erikras-richard-agent
Copy link
Contributor

Update: Verified that this bug is fixed in the current TypeScript codebase. The useEffect in useField.ts that registers the field now includes initialValue in its dependency array, so changes to initialValue trigger re-registration and a re-render.

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.

4 participants

Comments