Skip to content

Commit 8c50f95

Browse files
fix(core): use ref to prevent stale formData in rapid additional property renames (#5031)
* fix(core): use ref to prevent stale formData in rapid additional property renames * docs: add changelog entry for #5021 stale formData fix
1 parent d47c2b5 commit 8c50f95

File tree

3 files changed

+40
-3
lines changed

3 files changed

+40
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ should change the heading of the (upcoming) version to include a major version b
3535
- Included `button` elements in `focusOnError` querySelector so that radio and checkbox groups receive focus on validation error, fixing [#4870](https://github.com/rjsf-team/react-jsonschema-form/issues/4870)
3636
- Fixed focus being lost when renaming additional property keys by preserving React key for renamed properties ([#4999](https://github.com/rjsf-team/react-jsonschema-form/issues/4999))
3737
- Removed `expandUiSchemaDefinitions` call at form init, now handled at runtime by `resolveUiSchema`, fixing [#4986](https://github.com/rjsf-team/react-jsonschema-form/issues/4986)
38+
- Used `useRef` to track latest `formData` in `handleKeyRename`, preventing stale closure data when multiple additional property keys are renamed in quick succession, fixing [#5021](https://github.com/rjsf-team/react-jsonschema-form/issues/5021)
3839

3940
## @rjsf/daisyui
4041

packages/core/src/components/fields/ObjectField.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ export default function ObjectField<T = any, S extends StrictRJSFSchema = RJSFSc
220220
} = props;
221221
const { fields, schemaUtils, translateString, globalUiOptions } = registry;
222222
const { OptionalDataControlsField } = fields;
223+
const formDataRef = useRef(formData);
224+
formDataRef.current = formData;
223225
const schema: S = schemaUtils.retrieveSchema(rawSchema, formData, true);
224226
const uiOptions = getUiOptions<T, S, F>(uiSchema, globalUiOptions);
225227
const { properties: schemaProperties = {} } = schema;
@@ -310,9 +312,10 @@ export default function ObjectField<T = any, S extends StrictRJSFSchema = RJSFSc
310312
const handleKeyRename = useCallback(
311313
(oldKey: string, newKey: string) => {
312314
if (oldKey !== newKey) {
313-
const actualNewKey = getAvailableKey(newKey, formData);
315+
const currentFormData = formDataRef.current;
316+
const actualNewKey = getAvailableKey(newKey, currentFormData);
314317
const newFormData: GenericObjectType = {
315-
...(formData as GenericObjectType),
318+
...(currentFormData as GenericObjectType),
316319
};
317320
const newKeys: GenericObjectType = { [oldKey]: actualNewKey };
318321
const keyValues = Object.keys(newFormData).map((key) => {
@@ -321,14 +324,15 @@ export default function ObjectField<T = any, S extends StrictRJSFSchema = RJSFSc
321324
});
322325
const renamedObj = Object.assign({}, ...keyValues);
323326

327+
formDataRef.current = renamedObj as T;
324328
if (oldKey !== lastRenamedProperty.current.currentKey) {
325329
lastRenamedProperty.current.previousKey = oldKey;
326330
}
327331
lastRenamedProperty.current.currentKey = actualNewKey;
328332
onChange(renamedObj, childFieldPathId.path);
329333
}
330334
},
331-
[formData, onChange, childFieldPathId, getAvailableKey],
335+
[onChange, childFieldPathId, getAvailableKey],
332336
);
333337

334338
/** Handles the remove click which calls the `onChange` callback with the special ADDITIONAL_PROPERTY_FIELD_REMOVE

packages/core/test/ObjectField.test.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,6 +1163,38 @@ describe('ObjectField', () => {
11631163
);
11641164
});
11651165

1166+
it('should preserve all properties when two keys are renamed in quick succession', () => {
1167+
const formData = {
1168+
first: 1,
1169+
second: 2,
1170+
third: 3,
1171+
};
1172+
const { node, onChange } = createFormComponent({
1173+
schema,
1174+
formData,
1175+
});
1176+
1177+
const firstKeyNode = node.querySelector('#root_first-key');
1178+
const secondKeyNode = node.querySelector('#root_second-key');
1179+
1180+
act(() => {
1181+
fireEvent.blur(firstKeyNode!, {
1182+
target: { value: 'renamedFirst' },
1183+
});
1184+
fireEvent.blur(secondKeyNode!, {
1185+
target: { value: 'renamedSecond' },
1186+
});
1187+
});
1188+
1189+
expect(onChange).toHaveBeenCalledTimes(2);
1190+
expect(onChange).toHaveBeenLastCalledWith(
1191+
expect.objectContaining({
1192+
formData: { renamedFirst: 1, renamedSecond: 2, third: 3 },
1193+
}),
1194+
'root',
1195+
);
1196+
});
1197+
11661198
it('should preserve focus across consecutive renames of the same property', () => {
11671199
const { node } = createFormComponent({
11681200
schema,

0 commit comments

Comments
 (0)