All Posts

Managing Keys in React Form Array Fields

Keys are very important part of React/similar UI frameworks. Whenever we have a list of DOM nodes, React needs the key prop to identify every item in the list. You can think of these keys as the IDs of the rendered elements, similar to how we have/need our ID cards when we are standing in a queue.

You should checkout the React docs on Why does React need keys? for more details.

Problem with Indexes as Keys in Forms

If you are have Array Input Fields (Controlled) in the Forms in your React-like environment, you might be using the index or something similar for the key prop to pass the individual controlled field item.

This example allows you to get (random) rates for different hotels by their name. You can click on Add More to add more hotels and click on Remove to remove a hotel and type in the hotel’s name to get the rates. The rate field is reactive to the hotel’s name field.

We are using React Final Form but you can choose any available options. This is not and issue the form libraries.

Here we are using the index of the Array as the key for the individual item. The problem comes when we try to remove an item from the list. The removal of an item is causing rates to be refetched for all the following items.

import { Form, Field, FieldArray } from './Form';
import HotelRates from "./HotelRates"

export default function App() {
  return <Form 
    initialValues={{ hotels: initialValues }}>
    <FieldArray name="hotels">
      {({ fields }) => <>
        {fields.map((fieldName, index) => (
          <div key={index}>
            <label>Name</label>:&nbsp;
            <Field name={fieldName + ".name"} type="text" component="input" />
            <HotelRates name={fieldName + ".name"} />&nbsp;
            <button type="button" onClick={() => fields.remove(index)}>&times;</button><br /><br />
          </div>
        ))}
        <button type="button" onClick={() => fields.push({ name: ""})}>Add More</button>
      </>}
    </FieldArray>
    <br />
    <p style={{ color: "red"}}>Problem: Removal of an item (e.g. 2nd) will refetch to rates of all following items (e.g. 3rd,4th...)</p>
  </Form>
}

const initialValues = [
  { name: "Hotel 1"}, 
  { name: "Hotel 2"}, 
  { name: "Hotel 3"}
]

This is simple form but in reality you can imagine having a very nested array where values of some other (nested) fields are reactive to the values of some other fields.

Real-World Field Array

In one of my project, I have a form with array fields having 4 depth as shown below. This allows a user to retrieve the rates of Transportation based on dates and selected cabs.

type TTransportAndActivities = Array<{ // <- Field Array
  dates: Array<Date>,
  services: Array<{ // <- Field Array
    type: "transport" | "activity" // there are some other types as well
    service: {
        id: number,
        service_id: number,
        cabs: Array<{ // <- Field Array
          cab_type: string,
          no_of_cabs: number,
          date_wise_rates: Array<{ // <- Field Array
              date: string,
              rate: number, // rate fetched via an API
              custom_rate: number // customized rate entered by you (user)
          }>
        }>
    }
  }>
}>

In the form, if you change any data (e.g. dates, service_id, cab_type), it will result in following:

  1. refetch of the rates
  2. will reset the custom_rate field.

These two points are important. Essentially, these two points means that values of some other fields are reactive to the values of some other fields.

What is happening

As stated earlier, the keys are the identities of the items in a list. Because we are using the index as the key of the list items, removal of an ith item is replacing this item with i+1th because in the next redering process, i+1th item becomes the ith item.

Let’s understand it with an example. Suppose, you and I are standing in a queue to buy tickets and we are given a token number (key) which is our ordering number in the queue. I am standing right behind you. Support you get a token number as 5, and so I will get the 6 number.

# TICKET QUEUE
ID 1. 2. 3. 4. 5. 6. 7. 8. 9.
   |  |  |  |  |  |  |  |  |
               |  |
              You |
                 Me

Suddenly you get a call and have to leave for some urgent work, and so your leave. Now what happens with token numbers ? As you gussed, I get the number 5 and all other behind me follow with the new token number (old - 1) but the people ahead of me stays the same.

# TICKET QUEUE
ID 1. 2. 3. 4. 5. 6. 7. 8.
   |  |  |  |  |  |  |  | 
               |
               |
              Me
              

Now, there is someone who is inspecting the queue. This person will notice as if all the people from token 5 onwards have been changed, instead of the reality, which is that the number 5 is gone and other are the same.

This inspector is React in the case of our list items and it will see as if the items has been updated, instead of an item being remove. Ofcourse as the item is removed, the length is not smaller and so the very last index from earlier has been updated, which doen’t matter as there is no item there any longer.

In our example, when we remove an item (say 2nd and hotel’s name being Hotel 2), the following (3rd and hotel’s name being Hotel 3) item will get its key. React will see as if someone has updated it’s (2nd’s) content (hotel’s name) with the new content (from name Hotel 2 to Hotel 3) and so the rates will be refetched.

To tests that the rates are getting refetched because of content change, you can do the following:

  1. Set the name of hotel in 1st and 2nd item to “Hotel A”.
  2. Set the name of the 3rd item to something else e.g. “Hotel XYZ”.
  3. Now remove the first item.

You will see that

  1. The rate for the new first item will NOT be fetched. It will keep the rates from older deleted item (1st), instead of the rate of the replacing item.
  2. The rate for new second item will be fetched.

Solution

Instead of relying on the index of the list item, we can associate an identity to every list item by adding a new field (e.g. __id).

import { Form, Field, FieldArray, getIn, generateId } from './Form';
import HotelRates from "./HotelRates"

export default function App() {
  return <Form 
    initialValues={{ hotels: initialValues }}>
    {({ form }) => <><FieldArray name="hotels">
      {({ fields }) => <>
        {fields.map((fieldName, index) => (
          <div key={getIn(form.getState().values, fieldName + ".__id")}>
            <label>Name</label>:&nbsp;
            <Field name={fieldName + ".name"} type="text" component="input" />
            <HotelRates keyValue={getIn(form.getState().values, fieldName + ".__id")} name={fieldName + ".name"} />&nbsp;
            <button type="button" onClick={() => fields.remove(index)}>&times;</button><br /><br />
          </div>
        ))}
        <button type="button" onClick={() => fields.push({ __id: generateId(), name: ""})}>Add More</button>
      </>}
    </FieldArray>
    <br />
    <p style={{ color: "green"}}>Removal of an item (e.g. 2nd) will <b>NOT</b> refetch to rates of all following items (e.g. 3rd,4th...)</p>
    </>}
  </Form>
}

const initialValues = [
  { __id: generateId(), name: "Hotel 1"}, 
  { __id: generateId(), name: "Hotel 2"}, 
  { __id: generateId(), name: "Hotel 3"}
]

Some Form libraries like React Hook Form provides ways to handle the keys for array fields with some constraints on how to use field array.