diff --git a/packages/react/src/JsonFormsContext.tsx b/packages/react/src/JsonFormsContext.tsx
index 5bb6ee2cbf..cbb911533c 100644
--- a/packages/react/src/JsonFormsContext.tsx
+++ b/packages/react/src/JsonFormsContext.tsx
@@ -70,7 +70,8 @@ import {
mapDispatchToArrayControlProps,
i18nReducer
} from '@jsonforms/core';
-import React, { ComponentType, Dispatch, ReducerAction, useContext, useEffect, useMemo, useReducer, useRef } from 'react';
+import debounce from 'lodash/debounce';
+import React, { ComponentType, Dispatch, ReducerAction, useCallback, useContext, useEffect, useMemo, useReducer, useRef } from 'react';
const initialCoreState: JsonFormsCore = {
data: {},
@@ -110,7 +111,7 @@ const useEffectAfterFirstRender = (
export const JsonFormsStateProvider = ({ children, initState, onChange }: any) => {
const { data, schema, uischema, ajv, validationMode } = initState.core;
- // Initialize core immediately
+
const [core, coreDispatch] = useReducer(
coreReducer,
undefined,
@@ -165,8 +166,28 @@ export const JsonFormsStateProvider = ({ children, initState, onChange }: any) =
onChangeRef.current = onChange;
}, [onChange]);
+ /**
+ * A common pattern for users of JSON Forms is to feed back the data which is emitted by
+ * JSON Forms to JSON Forms ('controlled style').
+ *
+ * Every time this happens, we dispatch the 'updateCore' action which will be a no-op when
+ * the data handed over is the one which was just recently emitted. This allows us to skip
+ * rerendering for all normal cases of use.
+ *
+ * However there can be extreme use cases, for example when using Chrome Auto-fill for forms,
+ * which can cause JSON Forms to emit multiple change events before the parent component is
+ * rerendered. Therefore not the very recent data, but the previous data is fed back to
+ * JSON Forms first. JSON Forms recognizes that this is not the very recent data and will
+ * validate, rerender and emit a change event again. This can then lead to data loss or even
+ * an endless rerender loop, depending on the emitted events chain.
+ *
+ * To handle these edge cases in which many change events are sent in an extremely short amount
+ * of time we debounce them over a short amount of time. 10ms was chosen as this worked well
+ * even on low-end mobile device settings in the Chrome simulator.
+ */
+ const debouncedEmit = useCallback(debounce((...args) => onChangeRef.current?.(...args), 10), []);
useEffect(() => {
- onChangeRef.current?.({ data: core.data, errors: core.errors });
+ debouncedEmit({ data: core.data, errors: core.errors });
}, [core.data, core.errors]);
return (
@@ -216,7 +237,6 @@ export const ctxToOneOfEnumControlProps = (ctx: JsonFormsStateContext, props: Ow
*/
const options = useMemo(() => enumProps.options, [props.options, enumProps.schema]);
return {...enumProps, options}
-
}
export const ctxToMultiEnumControlProps = (ctx: JsonFormsStateContext, props: OwnPropsOfControl) =>
diff --git a/packages/react/test/renderers/JsonForms.test.tsx b/packages/react/test/renderers/JsonForms.test.tsx
index ad4fce041f..46b50783ed 100644
--- a/packages/react/test/renderers/JsonForms.test.tsx
+++ b/packages/react/test/renderers/JsonForms.test.tsx
@@ -865,7 +865,7 @@ test('JsonForms should not crash with undefined uischemas', () => {
);
});
-test('JsonForms should call onChange handler with new data', () => {
+test('JsonForms should call onChange handler with new data', (done) => {
const onChangeHandler = jest.fn();
const TestInputRenderer = withJsonFormsControlProps(props => (
props.handleChange('foo', ev.target.value)} />
@@ -893,13 +893,18 @@ test('JsonForms should call onChange handler with new data', () => {
}
});
- const calls = onChangeHandler.mock.calls;
- const lastCallParameter = calls[calls.length - 1][0];
- expect(lastCallParameter.data).toEqual({ foo: 'Test Value' });
- expect(lastCallParameter.errors).toEqual([]);
+ // events are debounced for some time, so let's wait
+ setTimeout(() => {
+ const calls = onChangeHandler.mock.calls;
+ const lastCallParameter = calls[calls.length - 1][0];
+ expect(lastCallParameter.data).toEqual({ foo: 'Test Value' });
+ expect(lastCallParameter.errors).toEqual([]);
+ done();
+ }, 50);
+
});
-test('JsonForms should call onChange handler with errors', () => {
+test('JsonForms should call onChange handler with errors', (done) => {
const onChangeHandler = jest.fn();
const TestInputRenderer = withJsonFormsControlProps(props => (
props.handleChange('foo', ev.target.value)} />
@@ -938,11 +943,16 @@ test('JsonForms should call onChange handler with errors', () => {
}
});
- const calls = onChangeHandler.mock.calls;
- const lastCallParameter = calls[calls.length - 1][0];
- expect(lastCallParameter.data).toEqual({ foo: 'xyz' });
- expect(lastCallParameter.errors.length).toEqual(1);
- expect(lastCallParameter.errors[0].keyword).toEqual('minLength');
+ // events are debounced for some time, so let's wait
+ setTimeout(() => {
+ const calls = onChangeHandler.mock.calls;
+ const lastCallParameter = calls[calls.length - 1][0];
+ expect(lastCallParameter.data).toEqual({ foo: 'xyz' });
+ expect(lastCallParameter.errors.length).toEqual(1);
+ expect(lastCallParameter.errors[0].keyword).toEqual('minLength');
+ done();
+ }, 50);
+
});
test('JsonForms should update if data prop is updated', () => {