Skip to content

Commit ecc0c30

Browse files
author
Erik Rasmussen
committed
Fix #869: Synchronize field name and value when name changes dynamically
1 parent 21d9f3b commit ecc0c30

File tree

2 files changed

+106
-2
lines changed

2 files changed

+106
-2
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
import * as React from 'react'
5+
import { render, fireEvent, waitFor } from '@testing-library/react'
6+
import '@testing-library/jest-dom'
7+
import Form from './ReactFinalForm'
8+
import Field from './Field'
9+
import { act } from 'react-dom/test-utils'
10+
11+
describe('useField - Dynamic Name (Issue #869)', () => {
12+
it('should keep name and value in sync when field name changes', async () => {
13+
const renderSpy = jest.fn()
14+
15+
const TestComponent = ({ fieldName }: { fieldName: string }) => {
16+
return (
17+
<Form
18+
onSubmit={() => {}}
19+
initialValues={{ a: 'value-a', b: 'value-b' }}
20+
>
21+
{() => (
22+
<Field name={fieldName}>
23+
{({ input }) => {
24+
// Log every render to track name/value sync
25+
renderSpy(input.name, input.value)
26+
return <input {...input} data-testid="field" />
27+
}}
28+
</Field>
29+
)}
30+
</Form>
31+
)
32+
}
33+
34+
const { rerender } = render(<TestComponent fieldName="a" />)
35+
36+
// Initial render - field 'a'
37+
expect(renderSpy).toHaveBeenCalledWith('a', 'value-a')
38+
39+
renderSpy.mockClear()
40+
41+
// Change field name from 'a' to 'b'
42+
act(() => {
43+
rerender(<TestComponent fieldName="b" />)
44+
})
45+
46+
// BUG: First render after name change has mismatched name/value
47+
// We get name='b' but value='value-a' (stale)
48+
const calls = renderSpy.mock.calls
49+
50+
// The bug manifests as: first call has name='b' but value='value-a'
51+
// Expected: ALL calls should have name and value in sync
52+
calls.forEach(call => {
53+
const [name, value] = call
54+
if (name === 'a') {
55+
expect(value).toBe('value-a')
56+
} else if (name === 'b') {
57+
expect(value).toBe('value-b') // This will FAIL on first render
58+
}
59+
})
60+
})
61+
62+
it('should have correct value immediately after name change (no stale renders)', async () => {
63+
const TestComponent = ({ fieldName }: { fieldName: string }) => {
64+
return (
65+
<Form
66+
onSubmit={() => {}}
67+
initialValues={{ a: 'value-a', b: 'value-b' }}
68+
>
69+
{() => (
70+
<Field name={fieldName}>
71+
{({ input }) => (
72+
<div>
73+
<span data-testid="name">{input.name}</span>
74+
<span data-testid="value">{input.value}</span>
75+
</div>
76+
)}
77+
</Field>
78+
)}
79+
</Form>
80+
)
81+
}
82+
83+
const { rerender, getByTestId } = render(<TestComponent fieldName="a" />)
84+
85+
expect(getByTestId('name')).toHaveTextContent('a')
86+
expect(getByTestId('value')).toHaveTextContent('value-a')
87+
88+
// Change field name
89+
act(() => {
90+
rerender(<TestComponent fieldName="b" />)
91+
})
92+
93+
// IMMEDIATELY after rerender, name and value should be in sync
94+
expect(getByTestId('name')).toHaveTextContent('b')
95+
expect(getByTestId('value')).toHaveTextContent('value-b') // BUG: This will show 'value-a'
96+
})
97+
})

src/useField.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,11 @@ function useField<
187187
const meta: any = {};
188188
addLazyFieldMetaState(meta, state);
189189
const getInputValue = () => {
190-
let value = state.value;
190+
// Fix #869: If name changed but state hasn't updated yet (effect hasn't run),
191+
// get the value directly from form values to avoid returning stale value
192+
let value = state.name !== name
193+
? getIn(form.getState().values, name)
194+
: state.value;
191195

192196
// Handle null values first
193197
if (value === null && !allowNull) {
@@ -221,7 +225,10 @@ function useField<
221225
};
222226

223227
const getInputChecked = () => {
224-
let value = state.value;
228+
// Fix #869: Same as getInputValue - sync with current name
229+
let value = state.name !== name
230+
? getIn(form.getState().values, name)
231+
: state.value;
225232
if (type === "checkbox") {
226233
value = parse(value, name);
227234
if (_value === undefined) {

0 commit comments

Comments
 (0)