jaredpalmer / formik

Build forms in React, without the tears 😭
https://formik.org
Apache License 2.0
33.94k stars 2.79k forks source link

FastFields rerender needlesly inside FieldArray #1895

Open AlanKrygowski opened 5 years ago

AlanKrygowski commented 5 years ago

🐛 Bug report

Current Behavior

I have a FastField and Field group inside a component wrapped inside a FieldArray. Every change I make to any of the Fields causes rerenders in all of the fields (Including the FastFields.

Expected behavior

Making changes in the FastField should not trigger re-renders in the Fields and other FastFields.

Reproducible example

Here is prototype example where the issue can be seen https://codesandbox.io/s/distracted-hamilton-uf6cb?fontsize=14.

The re-renders were found when opening the page in a fullscreen tab, and checking them with the React Dev Tools. Here's a screencast of the rerenders: https://drive.google.com/file/d/1ydVG0U8rAhzzzec0tv7k6M-X4qJlHY2B/view

The top right input for each array element and the textarea inputs are the mentioned FastFields

Additional context

The form I'm working on is a rather big form. The example here is a rather basic version of it. The form is based around entries that are each stored inside the FieldArray.

Your environment

The ENV is a standard codesandbox enviorment with formik added as a dependency. All is the most up to date, since the prototype was created two days ago.

Sidenote

I wasn't able to find any information about this but: Is there a way to ensure the individual fields of the FieldArray component would not trigger re-rendering of the the other field array fields?

AlanKrygowski commented 4 years ago

Not sure whether bumping things here is punishable by death....but the issue still exists, and really is the only thing blocking me from convincing my peers to switch our form handling to formik :L

mikecsmith commented 4 years ago

@AlanKrygowski - I was able to solve this re-render issue in a slightly unorthodox way. The issue with your code sandbox example - and with the form I was working on is that you've effectively got a piece of state in Formik that controls your entries. Then you've got your <FieldArray> component and inside that you're mapping your entries to render the various form fields. The issue is that these fields then set their own state on entries - i.e you've got:

const initialValues = {
   entries: [],
}

const EntryItem = ({namePrefix}) => <Field name={`${namePrefix}.name`} component={input} />

const MyForm = () => {
  return(
    {/*}...Left out for brevity...*/}
    <FieldArray>
      {arrayHelpers => (
        <>
          {values.entries.map((_, idx) => (
            <EntryItem
              key={idx}
              namePrefix={`entries.${idx}`}
            />
          ))}
          <Button onClick={() => arrayHelpers.push("Placeholder String")}>
            Add Entry
          </Button>
        </>
      )}
    </FieldArray>
  );
}

Each time you type into <EntryItem /> you're triggering an update to values.entries.whatever which is causing the map to fire again and re-rendering the component.

How I solved the issue is to do the following (NB: this is just example code - actual has a lot more boilerplate):

const initialValues = {
   entries: [],
   entryValues: {}
}

const EntryItem = React.memo(({namePrefix}) => <Field name={`${namePrefix}.name`} component={input} />);

const MyForm = () => {
  return(
    {/*}...Left out for brevity...*/}
    <FieldArray>
      {arrayHelpers => (
        <>
          {values.entries.map((entry) => (
            <EntryItem
              key={entry}
              namePrefix={`entryValues.${entry}`}
            />
          ))}
          <Button onClick={() => {
            arrayHelpers.push(values.entries[values.entries.length - 1] + 1 || 0); // This gets the last value in the array and adds 1 to prevent duplicate keys. If there are no items in the array it initializes it at 0.
            setFieldValue("entryValues", {
              ...values.entryValues,
              [values.entries.length]: "Placeholder String"
            });
          }}>
            Add Entry
          </Button>
        </>
      )}
    </FieldArray>
  );
}

So what's happening here is you're using the entries array as a place to store keys (based on the length of your entries array) for values which are stored on a separate object. Think of it as being similar to normalized state in Redux i.e. allIds, byId.

Because you're updating the values inside entryValues (which is not being mapped) the components are not re-rendered (the memo helps ensure this) except when adding to or deleting from the entries array.

You do have to do a bit of work if you want to add the ability to remove entries from the field array as well - but that basically involves passing the arrayHelpers and setFieldValue down into the EntryItem and updating the value of entryValues and your array of key indexes to remove any unneeded values.

Hope that helps!

Quick edit to cover removals - removing items from the values object is tricky - best I've come up with so far is to set the values on the keys as undefined.

justinstigall commented 4 years ago

I've got the same issue, every FastField in my form is fairly fast except for inside of FieldArray

karanaditya993 commented 3 years ago

Hello! First and foremost, thank you for all the hard work on formik, it is a great library that I've used often and really appreciate it's ease of use. Wondering if this is being worked on or if there is an ETA on the implementation? Running into similar issues where <FieldArray /> does not utilize the performance optimization of <FastField />, especially within the context of large forms (i.e. 30+ fields). Thanks very much.

jasonwilliams commented 3 years ago

👋 I've been experiencing this issue also. We get a lot of re-rendering with FieldArray causing slowness. I decided to take a look at the code. It seems to re-render regardless of the child component's state. I've added a PR which addresses this, it adds the same functionality as FastField i.e you can now pass a shouldUpdate method to it which controls when it should re-render. If not it will check changes between the props.

For now ive added the functionality to the FieldArray but it may need to be a new FastFieldArray depending on what the maintainers here think.

https://github.com/formium/formik/pull/3295