Skip to content

Commit 49d7d6c

Browse files
authored
fix(ui): snapshot pre-append and fallback setValue to fix add-item with default 0 in ArrayField (#139)
1 parent ad10dc8 commit 49d7d6c

File tree

2 files changed

+37
-1
lines changed

2 files changed

+37
-1
lines changed

.changeset/polite-words-laugh.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openzeppelin/contracts-ui-builder-ui': patch
3+
---
4+
5+
snapshot pre-append and fallback setValue to fix add-item with default 0 in ArrayField

packages/ui/src/components/fields/ArrayField.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,38 @@ export function ArrayField<TFieldValues extends FieldValues = FieldValues>({
109109
? false
110110
: ''; // Default for text, address, etc.
111111

112+
// Background and rationale:
113+
// In certain runtime states (e.g., preview remounts, incidental resets, StrictMode double-invocations),
114+
// an immediate read right after useFieldArray.append(...) can be stale and still reflect the
115+
// pre-append array. This manifested as a user-visible bug where adding an item (notably value 0)
116+
// either did nothing or briefly appeared and then disappeared.
117+
//
118+
// To make the operation deterministic, we snapshot the array BEFORE calling append and then
119+
// verify that the length actually increased. If not, we force-set the array using the snapshot
120+
// plus the new value. This is safe and idempotent because it only runs when the immediate read
121+
// did not reflect the append; it does not double-append.
122+
const fieldsBeforeAppend = fields.length;
123+
const valuesBeforeAppend = (formContext.getValues(name) as unknown[] | undefined) ?? [];
124+
112125
append(defaultValue as FieldValues[typeof name]);
126+
127+
// Verify append actually reflected in form state; if not, force set
128+
const afterAppendFieldsCount =
129+
(formContext.getValues(name) as unknown[] | undefined)?.length ?? fields.length;
130+
131+
// Fallback: if the immediate read did not reflect the append, coerce to
132+
// "previous snapshot + new value" so the UI consistently renders the new item.
133+
if (afterAppendFieldsCount <= fieldsBeforeAppend) {
134+
const baseArray = Array.isArray(valuesBeforeAppend)
135+
? (valuesBeforeAppend as unknown[])
136+
: ([] as unknown[]);
137+
const coercedArray = [...baseArray, defaultValue] as unknown[];
138+
139+
formContext.setValue(name as never, coercedArray as never, {
140+
shouldDirty: true,
141+
shouldTouch: true,
142+
});
143+
}
113144
};
114145

115146
// Check if we can add more items
@@ -182,7 +213,7 @@ export function ArrayField<TFieldValues extends FieldValues = FieldValues>({
182213
fields.map((field, index) => {
183214
// Create field configuration for array element
184215
const elementField: FormFieldType = {
185-
id: `${id}-${index}`, // Internal ID for the element's field
216+
id: field.id, // Use RHF field.id for stable, unique keys per item
186217
name: `${name}.${index}`, // RHF name for the element
187218
label: elementFieldConfig?.label || '', // Label for the element's field (can be empty if not desired)
188219
type: elementType,

0 commit comments

Comments
 (0)