Skip to content

Commit c916e5f

Browse files
committed
Debounce emitted events in React
There are some use cases in which a large amount of independent changes are performed in an extremely low amount of time, potentially leading to data loss or endless rerendering loops when using the React bindings. An example for such a use case is Chrome auto-fill which can cause JSON Forms to emit multiple change events before the parent component is rerendered. If the parent component feeds the emitted data back to JSON Forms then it will hand over not the latest data, but the previouslys emitted data first. JSON Forms recognizes that this is not the very recent data and will validate, rerender and emit a change event again, leading to the problematic behavior. 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. We debounce the emitted events instead of the incoming data as we don't know anything about the amount of work performed (and therefore time passed) on the emitted data.
1 parent 64147e1 commit c916e5f

File tree

1 file changed

+24
-4
lines changed

1 file changed

+24
-4
lines changed

packages/react/src/JsonFormsContext.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ import {
7070
mapDispatchToArrayControlProps,
7171
i18nReducer
7272
} from '@jsonforms/core';
73-
import React, { ComponentType, Dispatch, ReducerAction, useContext, useEffect, useMemo, useReducer, useRef } from 'react';
73+
import debounce from 'lodash/debounce';
74+
import React, { ComponentType, Dispatch, ReducerAction, useCallback, useContext, useEffect, useMemo, useReducer, useRef } from 'react';
7475

7576
const initialCoreState: JsonFormsCore = {
7677
data: {},
@@ -110,7 +111,7 @@ const useEffectAfterFirstRender = (
110111

111112
export const JsonFormsStateProvider = ({ children, initState, onChange }: any) => {
112113
const { data, schema, uischema, ajv, validationMode } = initState.core;
113-
// Initialize core immediately
114+
114115
const [core, coreDispatch] = useReducer(
115116
coreReducer,
116117
undefined,
@@ -165,8 +166,28 @@ export const JsonFormsStateProvider = ({ children, initState, onChange }: any) =
165166
onChangeRef.current = onChange;
166167
}, [onChange]);
167168

169+
/**
170+
* A common pattern for users of JSON Forms is to feed back the data which is emitted by
171+
* JSON Forms to JSON Forms ('controlled style').
172+
*
173+
* Every time this happens, we dispatch the 'updateCore' action which will be a no-op when
174+
* the data handed over is the one which was just recently emitted. This allows us to skip
175+
* rerendering for all normal cases of use.
176+
*
177+
* However there can be extreme use cases, for example when using Chrome Auto-fill for forms,
178+
* which can cause JSON Forms to emit multiple change events before the parent component is
179+
* rerendered. Therefore not the very recent data, but the previous data is fed back to
180+
* JSON Forms first. JSON Forms recognizes that this is not the very recent data and will
181+
* validate, rerender and emit a change event again. This can then lead to data loss or even
182+
* an endless rerender loop, depending on the emitted events chain.
183+
*
184+
* To handle these edge cases in which many change events are sent in an extremely short amount
185+
* of time we debounce them over a short amount of time. 10ms was chosen as this worked well
186+
* even on low-end mobile device settings in the Chrome simulator.
187+
*/
188+
const debouncedEmit = useCallback(debounce((...args) => onChangeRef.current?.(...args), 10), []);
168189
useEffect(() => {
169-
onChangeRef.current?.({ data: core.data, errors: core.errors });
190+
debouncedEmit({ data: core.data, errors: core.errors });
170191
}, [core.data, core.errors]);
171192

172193
return (
@@ -216,7 +237,6 @@ export const ctxToOneOfEnumControlProps = (ctx: JsonFormsStateContext, props: Ow
216237
*/
217238
const options = useMemo(() => enumProps.options, [props.options, enumProps.schema]);
218239
return {...enumProps, options}
219-
220240
}
221241

222242
export const ctxToMultiEnumControlProps = (ctx: JsonFormsStateContext, props: OwnPropsOfControl) =>

0 commit comments

Comments
 (0)