Formik: FastFields rerender needlesly inside FieldArray

Created on 18 Oct 2019  路  3Comments  路  Source: formium/formik

馃悰 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?

FastField FieldArray Performance Bug

Most helpful comment

@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.

All 3 comments

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

@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.

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

PeerHartmann picture PeerHartmann  路  3Comments

ancashoria picture ancashoria  路  3Comments

outaTiME picture outaTiME  路  3Comments

najisawas picture najisawas  路  3Comments

dfee picture dfee  路  3Comments