react-admin icon indicating copy to clipboard operation
react-admin copied to clipboard

useFieldArray append() does not update ArrayInput form element

Open ZachSelindh opened this issue 3 years ago • 7 comments

What you were expecting: Using react-hook-form's append() method from the useFieldArray hook should, when it properly updates form state with new line values, also update the ArrayInput / SimpleFormIterator to reflect those new values

What happened instead: The form values update as expected under the hood, but the form elements do not expand to accommodate the newly appended values.

Steps to reproduce: Use the append() method to add line items to an ArrayInput.

Related code: https://codesandbox.io/s/weathered-field-7xfw6e?file=/src/posts/PostCreate.tsx

Environment

  • React-admin version: 4.0.3
  • Last version that did not exhibit the issue (if applicable): 3.19
  • React version: 17
  • Browser: Chrome
  • Stack trace (in case of a JS error):

ZachSelindh avatar May 06 '22 16:05 ZachSelindh

Hi! I think that using useFieldArray yourself somehow conflicts with the control declared by react-admin inside the ArrayInput. I don't think that what you're trying to do is documented, but I recommend using const { add } = useSimpleFormIterator(); in replacement for const { append } = useFieldArray({ name: "backlinks" }); (inside the SimpleFormIterator of course).

Also, I notice you have both a useEffect adding backlinks at mount, as well as a backlinksDefaultValue declared on you ArrayInput. What is your use case?

Lastly, here is a simple POC using useSimpleFormIterator to add a backlink when you click on a button. https://codesandbox.io/s/misty-sea-zulhn7?file=/src/posts/PostCreate.tsx:2463-2524 Note that TS is saying that you cannot pass in values to add(), but actually you can.

slax57 avatar May 09 '22 09:05 slax57

@slax57 Thanks for looking into this! I left the defaultValues prop in place so I could observe how it related to/interacted with the append method.

I'll try the useSimpleFormIterator hook, and report back my results.

Our use case is fairly simple, and used across many of our forms. But one example is; if a user is creating a work order, and that work order might require labor from multiple parties, we want to start the form with a labor line item for the current user, and then allow them to add additional line items as needed.

Perhaps an update to the docs is in order? They make reference to using useFormState and useFormContext to access/change form state, which led me to the assumption that useFieldArray would work the same for accessing the lines form level (after using setValue on the line level was SERIOUSLY non performant, which react-hook-form's docs make clear is the use case for useFieldArray).

ZachSelindh avatar May 09 '22 14:05 ZachSelindh

@slax57 Actually it appears that useSimpleFormIterator, since it has to be used inside the form iterator itself, doesn't allow for my use-case since it can only be called inside of line forms that already exist, and I'm trying to populate those items from outside the iterator/line items that don't yet exist.

ZachSelindh avatar May 09 '22 15:05 ZachSelindh

Hi @ZachSelindh Sorry for the delay of my answer. To me, this use case is, unfortunately, not really supported by RA officially. The add function exposed by useSimpleFormIterator was more of a wild guess about how you could work around this, rather than a feature we support and should document. But I agree with you that my suggestion was wrong for your use case, since it requires you to be inside the SimpleFormIterator.

To be honest, I don't fully understand why your first implementation (using useFieldArray) does not work. I can only assume it's somehow conflicting with RA calling it as well in its inner code. I've made another POC, this time using the append function provided by RA, and with this one it works just fine: https://codesandbox.io/s/pedantic-moon-t64z82?file=/src/posts/PostCreate.tsx There must be something conflicting somewhere but I can't find what.

Anyway, I cannot really consider this to be a bug since we don't officially support this, so I'll label this as an enhancement request.

slax57 avatar May 11 '22 12:05 slax57

To complete @slax57's answer, if you need to use the useFieldArray hook directly, you probably need to build your own Form iterator. React-admin components are not designed to work for all use cases. Instead, we offer components that can easily be replaced by your own.

So feel free to open a PR to support what you describe, and we'll consider merging it to the core. But we won't work on it ourselves in the foreseeable future. And we'll close this issue in a few weeks from now if nobody opens a PR to fix it.

fzaninotto avatar May 11 '22 13:05 fzaninotto

@slax57 @fzaninotto Thank you, gentlemen!

I'll share my current workaround for posterity; Since the values I'm hoping to add automatically to my line items form depend on input from the base form, I've refactored these forms to not render the ArrayInput until the required base inputs are filled. Then, I store the values I want to add as state at a higher level. That state is then passed to the ArrayInput as the defaultValues prop. This renders astronomically faster than using setValue() or, indeed append() or add(). Then, if the user makes changes to the form that should cause changes in the default line items, I clear the default value state, call reset(), passing all form values EXCEPT the lines form values, then repeat the process of updating state to create defaultValues, which are passed to the newly reset lines form.

This solution is quite fast and stable in my experience so far, but if I run into issues, I'll certainly circle back.

ZachSelindh avatar May 11 '22 14:05 ZachSelindh

@slax57 @ZachSelindh so what I did was copy the SimpleFormIterator, and pass in a custom externalAppend function and now I can call addField externally and it works as if it's inside of the fieldarray.

Not the prettiest but it works!

const MyForm = () => {
  let addToMySource = item => {};
  
  return (
    <>
      <button onClick={() => addToMySource({ property: 123 })}>Append 123</button>
      ...
      <ArrayInput source="mySource">
        <MyFormIterator externalAppend={x => addToMySource = x}>
      </ArrayInput>
    </>
  )
}

And in MyFormIterator:

useEffect(() => {
    props.externalAppend(addField);
}, [])

And that's it, the append method calls addField which is the same as calling it inside the iterator.

davidhenley avatar Aug 10 '22 20:08 davidhenley