Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
138dd5a
Fix #1050: useField returns Form initialValues on first render
Feb 3, 2026
03ec690
Fix #1055: Prevent overwriting getter-only properties in renderCompon…
erikras Feb 4, 2026
dcf1964
DEBUG: Add logging to diagnose initialValues issue
Feb 4, 2026
b114fd1
Fix #1050: Register field synchronously to capture Form initialValues
Feb 4, 2026
6631137
Fix #1050: Use v6.5.9 synchronous registration approach
Feb 4, 2026
31bc98b
Remove precedence test - not part of original issue
Feb 4, 2026
69c3fbb
Fix failing tests: Update test expectations and pause validation
Feb 4, 2026
ac12635
Update Field tests for correct initial render behavior
Feb 4, 2026
6a763c7
Fix validation count: expect 3 calls instead of 2
Feb 4, 2026
e7eee71
Fix validation count: back to 2 (pauseValidation prevents sync call)
Feb 4, 2026
e3837b5
Fix #1055: Remove spreading of lazy state in FormSpy renderProps (#1059)
erikras-richard-agent Feb 4, 2026
76616e6
Fix #1050: useField returns Form initialValues on first render (#1060)
erikras-richard-agent Feb 4, 2026
19cbe2c
Fix: Support type='select' for multiple select defaulting to [] (#1061)
erikras-richard-agent Feb 6, 2026
c8e8fbc
Fix: Update test expectation for validation call count
erikras-dinesh-agent Feb 6, 2026
d8e1a56
Refactor: Use useSyncExternalStore for #1050 fix
erikras-dinesh-agent Feb 6, 2026
b27453b
Fix: Address CodeRabbit issues with useSyncExternalStore
erikras-dinesh-agent Feb 6, 2026
759d95c
Fix: Address CodeRabbit DRY, stale state, and nested path issues
erikras-dinesh-agent Feb 6, 2026
d7a8d90
Fix: Use Final Form's getIn and support defaultValue + live values
erikras-dinesh-agent Feb 6, 2026
5bc2c21
Fix: Use subscription callback state in getSnapshot
erikras-dinesh-agent Feb 6, 2026
e08e6ea
Fix: Default data to {} and update test for useSyncExternalStore
erikras-dinesh-agent Feb 6, 2026
31ad254
Add use-sync-external-store shim for React 16.8+ compatibility
erikras-dinesh-agent Feb 6, 2026
a708212
Fix: Separate initial from value in fallback state
erikras-dinesh-agent Feb 6, 2026
c52d897
Merge main into fix/issue-1050-usefield-undefined-initial
Feb 10, 2026
2e9a798
Fix select multiple handling for both component and type props
Feb 10, 2026
3cffe3e
Remove package-lock.json (using yarn, not npm)
Feb 10, 2026
01e23c5
Merge main - keep useSyncExternalStore implementation
Feb 10, 2026
aceb1b8
Fix CodeRabbit review comments
Feb 10, 2026
b4c1801
Remove unused imports and variables
Feb 10, 2026
093a2f0
Apply hasOwnProperty fix consistently and improve comment
Feb 10, 2026
0061dd8
Fix: Compute dirty in fallback FieldState by comparing value and initial
Feb 13, 2026
ef48a8d
Fix: Use custom isEqual comparator in buildFallbackFieldState
Feb 13, 2026
6d0f2eb
Fix: Add type to useEffect dependency array
Feb 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions src/useField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -508,4 +508,76 @@ describe("useField", () => {
expect(calls).toContain("test"); // At least one call with 'test'
expect(calls[calls.length - 1]).toBe(null); // Last call is null
});

it("should return Form initialValues on first render (fix #1050)", () => {
const renderSpy = jest.fn();

const MyField = () => {
const { input } = useField("username");
renderSpy(input.value);
return <input {...input} data-testid="username" />;
};

const { getByTestId } = render(
<Form onSubmit={onSubmitMock} initialValues={{ username: "erikras" }}>
{() => (
<form>
<MyField />
</form>
)}
</Form>,
);

// Critical: on the FIRST render, value should be "erikras" not undefined
expect(renderSpy.mock.calls[0][0]).toBe("erikras");
expect(getByTestId("username").value).toBe("erikras");
});

it("should prefer Form initialValues over field initialValue", () => {
const renderSpy = jest.fn();

const MyField = () => {
const { input } = useField("username", { initialValue: "fallback" });
renderSpy(input.value);
return <input {...input} data-testid="username" />;
};

const { getByTestId } = render(
<Form onSubmit={onSubmitMock} initialValues={{ username: "formLevel" }}>
{() => (
<form>
<MyField />
</form>
)}
</Form>,
);

// Form-level initialValues should take precedence
expect(renderSpy.mock.calls[0][0]).toBe("formLevel");
expect(getByTestId("username").value).toBe("formLevel");
});

it("should use field initialValue when Form initialValues doesnt have that field", () => {
const renderSpy = jest.fn();

const MyField = () => {
const { input } = useField("username", { initialValue: "fieldLevel" });
renderSpy(input.value);
return <input {...input} data-testid="username" />;
};

const { getByTestId } = render(
<Form onSubmit={onSubmitMock} initialValues={{ other: "value" }}>
{() => (
<form>
<MyField />
</form>
)}
</Form>,
);

// Field-level initialValue should be used as fallback
expect(renderSpy.mock.calls[0][0]).toBe("fieldLevel");
expect(getByTestId("username").value).toBe("fieldLevel");
});
});
45 changes: 35 additions & 10 deletions src/useField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,25 +97,50 @@ function useField<

// Initialize state with proper field state from Final Form without callbacks
const [state, setState] = React.useState<FieldState<any>>(() => {
// Get the current field state from Final Form without registering callbacks
const existingFieldState = form.getFieldState(name as keyof FormValues);

if (existingFieldState) {
// FIX #1050: Register field synchronously to get proper initial state
// that includes Form initialValues. This ensures useField returns the
// correct value on first render instead of undefined.
let initialFieldState: FieldState<any> | undefined;

const unregister = form.registerField(
name as keyof FormValues,
(fieldState) => {
// Capture the initial state on first registration
initialFieldState = fieldState;
},
subscription,
{
afterSubmit,
beforeSubmit: () => undefined,
data,
defaultValue,
getValidator: () => config.validate,
initialValue,
isEqual: (a: any, b: any) => (config.isEqual || defaultIsEqual)(a, b),
silent: true, // Silent registration to avoid triggering validation
validateFields,
}
);

// Immediately unregister - we'll re-register properly in useEffect
unregister();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// If we got initial state from registration, use it
if (initialFieldState) {
// If allowNull is true and the initial value was null, preserve it
// (and its formatted version is not null, meaning it was formatted away)
if (allowNull && existingFieldState.initial === null && existingFieldState.value !== null) {
if (allowNull && initialFieldState.initial === null && initialFieldState.value !== null) {
return {
...existingFieldState,
...initialFieldState,
value: null, // Force value back to null
initial: null, // Ensure our local state's 'initial' also reflects this
};
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return existingFieldState;
return initialFieldState;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

// If no existing state, create a proper initial state
// Fallback: create initial state manually (shouldn't normally reach here)
let initialStateValue = initialValue;
if (component === "select" && multiple && initialValue === undefined) {
if (component === "select" && multiple && initialStateValue === undefined) {
initialStateValue = [];
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

Expand Down
Loading