Fixes unshift and insert row not update field's state correctly#96
Fixes unshift and insert row not update field's state correctly#96Elecweb wants to merge 1 commit intofinal-form:masterfrom
Conversation
|
I had similar problems. After many experiments, I came to the conclusion that I should abandon using an array in favor of normalized data - an array of IDs and a data object. This also allowed me to solve the issues with the unshift and move mutators. it also helped me solve performance problems. |
|
@erikras Hello, could you have a look at this? |
|
I didn't succeed by introducing the suggested finalFormArray.ts file(https://gist.github.com/Elecweb/7f3478b01126e40ce98492de32d3f995), importing the "insert" function and putting it in Form's mutators...The errors are still there...Instead, i tried to manually "push and update fields value" in order to avoid using splice-relevant array API and succeed. |
|
Thanks for fixing this long-standing bug! 🙏 Bug: Your fix: Observations: Code review question: @erikras - This fixes a 5+ year old bug. Worth reviewing if the fix is correct. @Elecweb - If you're still interested, please rebase against master. The community would appreciate this fix! 💙 |
Fixes #44 (implements solution from PR #96) ## Problem When insert() or unshift() is called on an array field, React reuses the component at the insertion index because the 'name' prop stays the same. This causes stale data to appear and field updates to fail. Example: If foo[0] and foo[1] exist, and you unshift a new item: - Values correctly become: [NEW, old-foo[0], old-foo[1]] - But field state for foo[0] still has old-foo[0]'s data - React doesn't remount the component, so registerField doesn't fire - Result: foo[0] shows old data and doesn't respond to changes ## Solution When shifting fields at index >= insertionIndex: 1. Copy field state to the incremented position (foo[0] → foo[1]) 2. Keep the original field at the insertion index (foo[0]) 3. Reset that field to default state via resetFieldState() This ensures the field at the insertion index has fresh state, forcing React to recognize it as a new field when the component re-renders. ## Tests - Updated insert.test.ts to expect field at insertion index - Updated unshift.test.ts to expect field at insertion index - Added unshift.regression-44.test.ts with regression tests All 68 tests passing ✅ Credit: Solution from @Elecweb's PR #96
* Fix #44: Reset field state at insertion index in insert/unshift Fixes #44 (implements solution from PR #96) ## Problem When insert() or unshift() is called on an array field, React reuses the component at the insertion index because the 'name' prop stays the same. This causes stale data to appear and field updates to fail. Example: If foo[0] and foo[1] exist, and you unshift a new item: - Values correctly become: [NEW, old-foo[0], old-foo[1]] - But field state for foo[0] still has old-foo[0]'s data - React doesn't remount the component, so registerField doesn't fire - Result: foo[0] shows old data and doesn't respond to changes ## Solution When shifting fields at index >= insertionIndex: 1. Copy field state to the incremented position (foo[0] → foo[1]) 2. Keep the original field at the insertion index (foo[0]) 3. Reset that field to default state via resetFieldState() This ensures the field at the insertion index has fresh state, forcing React to recognize it as a new field when the component re-renders. ## Tests - Updated insert.test.ts to expect field at insertion index - Updated unshift.test.ts to expect field at insertion index - Added unshift.regression-44.test.ts with regression tests All 68 tests passing ✅ Credit: Solution from @Elecweb's PR #96 * Add regression test for #47 (submitErrors removal) Verifies that PR #48's fix continues to work correctly. Test confirms submitErrors are properly removed when array items are removed via fields.remove(). --------- Co-authored-by: Erik Rasmussen <erik@mini.local>

as reported in #44 and final-form/react-final-form-arrays#138.
This applied both
unshiftandinsertbecauseunshiftuseinsertas thisfinal-form-arrays/src/unshift.js
Lines 5 to 9 in fe3c260
I've investigated and have assumed the issue is in the following.
final-form-arrays/src/insert.js
Lines 18 to 35 in fe3c260
the logic is trying to shift the field's state to one for all fields that are greater than or equal to inserted index.
For the field's state at inserted index, it will leave as empty because there's a return statement here.
final-form-arrays/src/insert.js
Lines 26 to 37 in fe3c260
For example, if there're 4 field array items and we used
insert(2, value), the field's state will shifted like thisthis behavior, as I understand, it's intended and should be correct. because when the state is empty, it'll get the default state from here
https://github.com/final-form/final-form/blob/d20c44be8766b93424e7754fe2629820fd93d21a/src/FinalForm.js#L850-L866
which should set the default for the missing state. but the problem is related to React component's
keypropSince the component is rendered by each item in
fields, as a convention and it should be, provided withnamefromfields. so the field component is reused with the same component as the previous, one beforeinsert(2, value)is called and after becausenameare the same. So methodregisterFieldwhich should set the field's state as default, doesn't trigger. this makes the field's state incorrect and leads to rendering problems.I've tried to make the key unique and found that it render correctly. but as we know, this is not a good solution.
Screen.Recording.2566-06-19.at.00.30.43.mov
Here are the possible solutions I can think of
nameshould not be used as a key and the final form array provides another variable for this purpose instead.Since the problem is about the rendering of components, we might need to provide a
keywhich is different before and afterinsert. But I can't think of how we should generate a key that is new every time we insert a new index but also it should be generated based on an index. This is quite a conflict. so I have no idea about this. anyways, this should be done in https://github.com/final-form/final-form-arrays and shouldn't relate here.Just remove the shifting state and keep only copy value
as suggested but this Insert does not cause array children to refresh correctly. react-final-form-arrays#138 (comment) which I still do not understand why it works as the field's state at inserted index just missing. Anyways, It might be some edge cases that lead to errors.
Add default state for the inserted field.
IMO, this might be the safest solution, which try to add default as intended. Fortunately, in the mutator, there's a helper function
resetFieldStatefor resetting the field's state.My PR is to use solution 3, as far as I have checked, it has no problem. I checked with validation for the array, shows an error at submit (this is required the use of the field's state).
(the validation is, the name field must have string "t")
https://github.com/final-form/final-form-arrays/assets/13183413/5c055b9e-9046-49f9-8974-d98a99df5954
For anyone, who can't wait for the fix, and need a quick solution now (I can feel that), I provided the gist you can copy and use the functions as the mutator.
https://gist.github.com/Elecweb/7f3478b01126e40ce98492de32d3f995
I hope this can help anyone get some insight into the issue and solutions 🙏