Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 14 additions & 13 deletions src/Field.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -801,16 +801,18 @@ describe("Field", () => {
);
expect(red).toHaveBeenCalled();
expect(red).toHaveBeenCalledTimes(2);
expect(red.mock.calls[0][0].input.checked).toBe(false);
expect(red.mock.calls[1][0].input.checked).toBe(true); // Correctly true for "red" radio
// After fix #1050, initialValues work on first render
expect(red.mock.calls[0][0].input.checked).toBe(true); // Correctly true for "red" from initialValues
expect(red.mock.calls[1][0].input.checked).toBe(true);
expect(green).toHaveBeenCalled();
expect(green).toHaveBeenCalledTimes(2);
expect(green.mock.calls[0][0].input.checked).toBe(false);
expect(green.mock.calls[1][0].input.checked).toBe(false); // Correctly false for "green" radio
expect(green.mock.calls[1][0].input.checked).toBe(false); // Correctly false for "green"
expect(blue).toHaveBeenCalled();
expect(blue).toHaveBeenCalledTimes(2);
expect(blue.mock.calls[0][0].input.checked).toBe(false);
expect(blue.mock.calls[1][0].input.checked).toBe(true); // Correctly false for "blue" radio
// After fix #1050, initialValues work on first render
expect(blue.mock.calls[0][0].input.checked).toBe(true); // Correctly true for "blue" from initialValues
expect(blue.mock.calls[1][0].input.checked).toBe(true);
});

it("should render radio buttons with checked prop", () => {
Expand Down Expand Up @@ -884,8 +886,9 @@ describe("Field", () => {
expect(red.mock.calls[1][0].input.checked).toBe(false); // Correctly false for "red" radio
expect(green).toHaveBeenCalled();
expect(green).toHaveBeenCalledTimes(2);
expect(green.mock.calls[0][0].input.checked).toBe(false);
expect(green.mock.calls[1][0].input.checked).toBe(true); // Correctly true for "green" radio
// After fix #1050, initialValues work on first render
expect(green.mock.calls[0][0].input.checked).toBe(true); // Correctly true for "green" from initialValues
expect(green.mock.calls[1][0].input.checked).toBe(true);
expect(blue).toHaveBeenCalled();
expect(blue).toHaveBeenCalledTimes(2);
expect(blue.mock.calls[0][0].input.checked).toBe(false);
Expand Down Expand Up @@ -1008,12 +1011,10 @@ describe("Field", () => {
</Form>,
);

// React is stricter about select multiple validation, so we expect one warning
// about the select multiple value not being an array initially
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy.mock.calls[0][0]).toContain(
"The `%s` prop supplied to <select> must be an array if `multiple` is true",
);
// After fix #1050, initialValues work on first render, so select multiple
// correctly gets the array value from initialValues and no longer triggers
// React's "must be an array" warning
expect(errorSpy).toHaveBeenCalledTimes(0);

// Reset the spy to test the actual Field warnings
errorSpy.mockClear();
Expand Down
90 changes: 90 additions & 0 deletions src/useField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -508,4 +508,94 @@ 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 use field initialValue when Form initialValues doesn't have that field (fix #1050)", () => {
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");
});

it("should handle nested field paths in Form initialValues (fix #1050)", () => {
const renderSpy = jest.fn();
const MyField = () => {
const { input } = useField("user.name");
renderSpy(input.value);
return <input {...input} data-testid="nested" />;
};
const { getByTestId } = render(
<Form
onSubmit={onSubmitMock}
initialValues={{ user: { name: "erikras" } }}
>
{() => (
<form>
<MyField />
</form>
)}
</Form>,
);
// Should correctly resolve nested path on first render
expect(renderSpy.mock.calls[0][0]).toBe("erikras");
expect(getByTestId("nested").value).toBe("erikras");
});

it("should handle array field paths in Form initialValues (fix #1050)", () => {
const renderSpy = jest.fn();
const MyField = () => {
const { input } = useField("items[0].name");
renderSpy(input.value);
return <input {...input} data-testid="array" />;
};
const { getByTestId } = render(
<Form
onSubmit={onSubmitMock}
initialValues={{ items: [{ name: "Apple" }] }}
>
{() => (
<form>
<MyField />
</form>
)}
</Form>,
);
// Should correctly resolve array path on first render
expect(renderSpy.mock.calls[0][0]).toBe("Apple");
expect(getByTestId("array").value).toBe("Apple");
});
});
13 changes: 10 additions & 3 deletions src/useField.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from "react";
import { fieldSubscriptionItems } from "final-form";
import { fieldSubscriptionItems, getIn } from "final-form";
import type { FieldSubscription, FieldState, FormApi } from "final-form";
import type {
UseFieldConfig,
Expand Down Expand Up @@ -113,9 +113,16 @@ function useField<
return existingFieldState;
}

// FIX #1050: Check Form initialValues before falling back to field initialValue
// If no existing state, create a proper initial state
let initialStateValue = initialValue;
if (component === "select" && multiple && initialValue === undefined) {
const formState = form.getState();
// Use getIn to support nested field paths like "user.name" or "items[0].id"
const formInitialValue = getIn(formState.initialValues, name);

// Use Form initialValues if available, otherwise use field initialValue
let initialStateValue = formInitialValue !== undefined ? formInitialValue : initialValue;

if (component === "select" && multiple && initialStateValue === undefined) {
initialStateValue = [];
}

Expand Down