Skip to content

Commit 912c8f9

Browse files
authored
Fix #4962 (#4979)
* fix #4962 Fixed `validateForm()` clearing `extraErrors` from state when schema validation passes * review feedback * review changes #2
1 parent 694ff04 commit 912c8f9

File tree

3 files changed

+165
-12
lines changed

3 files changed

+165
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ should change the heading of the (upcoming) version to include a major version b
2626

2727
- Fixed `extraErrors` not displaying on first async set after submit, fixing [#4965](https://github.com/rjsf-team/react-jsonschema-form/issues/4965)
2828
- Updated multi-select ArrayFields to properly use the `items` uiSchema for enumerated options, fixing [#4955](https://github.com/rjsf-team/react-jsonschema-form/issues/4955)
29+
- Fixed `validateForm()` clearing `extraErrors` from state when schema validation passes, fixing [#4962](https://github.com/rjsf-team/react-jsonschema-form/issues/4962)
30+
2931

3032
## @rjsf/utils
3133

packages/core/src/components/Form.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,17 +1256,12 @@ export default class Form<
12561256
const { extraErrors, extraErrorsBlockSubmit, focusOnFirstError, onError } = this.props;
12571257
const { errors: prevErrors } = this.state;
12581258
const schemaValidation = this.validate(formData);
1259-
let errors = schemaValidation.errors;
1260-
let errorSchema = schemaValidation.errorSchema;
1261-
const schemaValidationErrors = errors;
1262-
const schemaValidationErrorSchema = errorSchema;
1263-
const hasError = errors.length > 0 || (extraErrors && extraErrorsBlockSubmit);
1259+
// Always merge extraErrors so they remain visible in state regardless of extraErrorsBlockSubmit.
1260+
const { errors, errorSchema } = extraErrors ? this.mergeErrors(schemaValidation, extraErrors) : schemaValidation;
1261+
// hasError gates submission: schema errors always block; extraErrors only block when
1262+
// extraErrorsBlockSubmit is set (non-breaking default: extraErrors are informational only).
1263+
const hasError = schemaValidation.errors.length > 0 || (extraErrors && extraErrorsBlockSubmit);
12641264
if (hasError) {
1265-
if (extraErrors) {
1266-
const merged = validationDataMerge(schemaValidation, extraErrors);
1267-
errorSchema = merged.errorSchema;
1268-
errors = merged.errors;
1269-
}
12701265
if (focusOnFirstError) {
12711266
if (typeof focusOnFirstError === 'function') {
12721267
focusOnFirstError(errors[0]);
@@ -1278,8 +1273,8 @@ export default class Form<
12781273
{
12791274
errors,
12801275
errorSchema,
1281-
schemaValidationErrors,
1282-
schemaValidationErrorSchema,
1276+
schemaValidationErrors: schemaValidation.errors,
1277+
schemaValidationErrorSchema: schemaValidation.errorSchema,
12831278
},
12841279
() => {
12851280
if (onError) {
@@ -1289,6 +1284,14 @@ export default class Form<
12891284
}
12901285
},
12911286
);
1287+
} else if (errors.length > 0) {
1288+
// Non-blocking extraErrors are present — update display state without triggering onError.
1289+
this.setState({
1290+
errors,
1291+
errorSchema,
1292+
schemaValidationErrors: [],
1293+
schemaValidationErrorSchema: {},
1294+
});
12921295
} else if (prevErrors.length > 0) {
12931296
this.setState({
12941297
errors: [],

packages/core/test/Form.test.tsx

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4877,6 +4877,154 @@ describe('validateForm()', () => {
48774877
errors = node.querySelectorAll('.error-detail');
48784878
expect(errors).toHaveLength(0);
48794879
});
4880+
4881+
it('Should keep non-blocking extraErrors in state when schema is valid and extraErrorsBlockSubmit is not set', () => {
4882+
const formRef = createRef<Form>();
4883+
const schema: RJSFSchema = {
4884+
type: 'object',
4885+
properties: {
4886+
foo: { type: 'string' },
4887+
},
4888+
};
4889+
const extraErrors = {
4890+
foo: {
4891+
__errors: ['async error for foo'],
4892+
},
4893+
} as unknown as ErrorSchema;
4894+
const props: NoValFormProps = {
4895+
ref: formRef,
4896+
schema,
4897+
formData: { foo: 'valid' },
4898+
extraErrors,
4899+
};
4900+
const { onError } = createFormComponent(props);
4901+
4902+
let result: boolean | undefined;
4903+
act(() => {
4904+
result = formRef.current!.validateForm();
4905+
});
4906+
4907+
// Should return true (non-blocking)
4908+
expect(result).toBe(true);
4909+
// extraErrors should remain visible in state
4910+
expect(formRef.current!.state.errors).toHaveLength(1);
4911+
expect(formRef.current!.state.errors[0].message).toBe('async error for foo');
4912+
expect(formRef.current!.state.errorSchema).toEqual(extraErrors);
4913+
// onError should NOT be called for non-blocking errors
4914+
expect(onError).not.toHaveBeenCalled();
4915+
});
4916+
4917+
it('Should return false and call onError when extraErrors are present with extraErrorsBlockSubmit set', () => {
4918+
const formRef = createRef<Form>();
4919+
const schema: RJSFSchema = {
4920+
type: 'object',
4921+
properties: {
4922+
foo: { type: 'string' },
4923+
},
4924+
};
4925+
const extraErrors = {
4926+
foo: {
4927+
__errors: ['blocking async error'],
4928+
},
4929+
} as unknown as ErrorSchema;
4930+
const props: NoValFormProps = {
4931+
ref: formRef,
4932+
schema,
4933+
formData: { foo: 'valid' },
4934+
extraErrors,
4935+
extraErrorsBlockSubmit: true,
4936+
};
4937+
const { onError } = createFormComponent(props);
4938+
4939+
let result: boolean | undefined;
4940+
act(() => {
4941+
result = formRef.current!.validateForm();
4942+
});
4943+
4944+
// Should return false (blocking)
4945+
expect(result).toBe(false);
4946+
// Merged errors should be in state
4947+
expect(formRef.current!.state.errors).toHaveLength(1);
4948+
expect(formRef.current!.state.errors[0].message).toBe('blocking async error');
4949+
// onError SHOULD be called
4950+
expect(onError).toHaveBeenCalledWith(
4951+
expect.arrayContaining([expect.objectContaining({ message: 'blocking async error' })]),
4952+
);
4953+
});
4954+
4955+
it('Should show both schema and extraErrors in state when schema is invalid regardless of extraErrorsBlockSubmit', () => {
4956+
const formRef = createRef<Form>();
4957+
const schema: RJSFSchema = {
4958+
type: 'object',
4959+
required: ['foo'],
4960+
properties: {
4961+
foo: { type: 'string' },
4962+
},
4963+
};
4964+
const extraErrors = {
4965+
foo: {
4966+
__errors: ['async error for foo'],
4967+
},
4968+
} as unknown as ErrorSchema;
4969+
const props: NoValFormProps = {
4970+
ref: formRef,
4971+
schema,
4972+
formData: {},
4973+
extraErrors,
4974+
// extraErrorsBlockSubmit intentionally omitted
4975+
};
4976+
createFormComponent(props);
4977+
4978+
let result: boolean | undefined;
4979+
act(() => {
4980+
result = formRef.current!.validateForm();
4981+
});
4982+
4983+
// Schema error blocks submission → false
4984+
expect(result).toBe(false);
4985+
// Both schema error and extra error should be in state
4986+
const errorMessages = formRef.current!.state.errors.map((e) => e.message);
4987+
expect(errorMessages).toContain("must have required property 'foo'");
4988+
expect(errorMessages).toContain('async error for foo');
4989+
});
4990+
4991+
it('Should clear extraErrors from state when extraErrors prop is removed and validateForm is called again', () => {
4992+
const formRef = createRef<Form>();
4993+
const schema: RJSFSchema = {
4994+
type: 'object',
4995+
properties: {
4996+
foo: { type: 'string' },
4997+
},
4998+
};
4999+
const extraErrors = {
5000+
foo: {
5001+
__errors: ['async error for foo'],
5002+
},
5003+
} as unknown as ErrorSchema;
5004+
const props: NoValFormProps = {
5005+
ref: formRef,
5006+
schema,
5007+
formData: { foo: 'valid' },
5008+
extraErrors,
5009+
};
5010+
const { rerender } = createFormComponent(props);
5011+
5012+
// First call: extraErrors should appear in state
5013+
act(() => {
5014+
formRef.current!.validateForm();
5015+
});
5016+
expect(formRef.current!.state.errors).toHaveLength(1);
5017+
5018+
// Rerender without extraErrors
5019+
rerender({ ...props, extraErrors: undefined });
5020+
5021+
// Second call: no extraErrors, no schema errors → state should be cleared
5022+
act(() => {
5023+
formRef.current!.validateForm();
5024+
});
5025+
expect(formRef.current!.state.errors).toHaveLength(0);
5026+
expect(formRef.current!.state.errorSchema).toEqual({});
5027+
});
48805028
});
48815029

48825030
describe('setFieldValue()', () => {

0 commit comments

Comments
 (0)