Why do JavaScript frameworks and libraries need keys?

Hello everyone! In this article I would like to talk about such a concept as “keys” in JavaScript frameworks and libraries; why they are used and how they help in working with DOM.

Often, during interviews they ask about this topic and often the answers come out like: “to prevent uncontrolled behavior” or “they need to be indicated, because they are something like unique identifiers” etc. Of course, these answers are correct on the one hand, but they do not reflect the main thing.

I'll try to bring some clarity to this topic by showing how this concept works in real examples and what code is behind it.

A year ago I already wrote an article on this topic, but it was quite superficial, although it reflected more or less the essence. In this article, I will try to reveal the concept in the most complete way.

For this article, I made a video that contains everything that will be described here. This will be, so to speak, a text version:

I will leave all the sources described here at the end of the article.

Basic concepts

Before we look at the topic of “keys”, it's worth defining some concepts, such as “cycle” and “static || dynamic data”. They will be used later in the description.

Cycle – is a syntactic technique used in various frameworks and libraries to display the dependence of DOM elements on data. In Vue.js, for example, the attribute is used for such a dependence v-for:

<template>
  <tr
    v-for="{ id, label } of rows"
    :key="id"
    :class="{ danger: id === selected }"
    :data-label="label"
    v-memo="[label, id === selected]"
  >
    <td class="col-md-1">{{ id }}</td>
    <td class="col-md-4">
      <a @click="select(id)">{{ label }}</a>
    </td>
    <td class="col-md-1">
      <a @click="remove(id)">
        <span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
      </a>
    </td>
    <td class="col-md-6"></td>
  </tr>
</template>

Also, functions can be used, as for example is done in Cample:

const eachComponent = each(
  "table-rows",
  ({ importedData }) => importedData.rows,
  `<tr key="{{row.id}}" class="{{stack.class}}">
    <td class="col-md-1">{{row.id}}</td>
    <td class="col-md-4"><a ::click="{{setSelected()}}" class="lbl">{{row.label}}</a></td>
    <td class="col-md-1"><a ::click="{{importedData.delete(row.id)}}" class="remove"><span class="remove glyphicon glyphicon-remove" aria-hidden='true'></span></a></td>
    <td class="col-md-6"></td>
  </tr>`,
  {
    valueName: "row",
    functionName: "updateTable",
    stackName: "stack",
    import: {
      value: ["rows", "delete"],
      exportId: "mainExport",
    },
    functions: {
      setSelected: [
        (setData, event, eachStack) => () => {
          const { setStack, clearStack } = eachStack;
          clearStack();
          setStack(() => {
            return { class: "danger" };
          });
        },
        "updateTable",
      ],
    },
  }
);

Many other things can be used as well. The same methods applied to an array, some keywords that are defined only in HTML syntax extensions, etc.

Static data – this is some information that will not change when working with the application. For example, constants in files like config.tswhere class strings, physical quantities, arrays with images, etc. will be located. According to the current data, the “cycle” will conditionally “pass” only once.

Dynamic data – this is some information that will change when working with the application. This can be either a counter in a timer, where the value will be updated every second, or an array of objects that comes from the API. Here the “cycle” will “pass” first once, and then “pass” conditionally twice to compare two data states with each other (the old value and the new one).

All these concepts will be used later when analyzing the algorithm.

The concept of “key”

A key is a unique value that is needed to bind a DOM node to specific data. Binding is the process of storing a reference node to a source value. The value is usually a unique identifier that comes from a database. But it can also be any other value that can be used in the application. only once. That is, random values ​​may not work, because the chance of generating a similar string or number is non-zero.

Often, the key is syntactically specified as an attribute, into which we pass a unique value. Let's say like this:

<li key="{{id}}"></li>

Or, similarly, in the format key:valueas in the object.

The difference between the algorithm with and without a key

The main purpose of working with the “cycle” is to display DOM nodes that depend on the input data. Let's say we have a task to display information about something in a table, where each object is a row. Moreover, this information comes from the server and on the client we must react to the request state and display UI components accordingly. In this case, an empty array initially comes to the input, which will be filled or replaced with a new one after the data has successfully arrived from the server, but for now, there should be no rows in the table.

This task is easiest to show in code, where we will try to implement a simple case with data in pure js.

const data = [
  {
    id: 1,
    label: "Текст 1",
  },
  { id: 2, label: "Текст 2" },
  {
    id: 3,
    label: "Текст 3",
  },
];

const newData = [
  {
    id: 1,
    label: "Текст 1",
  },
  {
    id: 3,
    label: "Текст 3",
  },
];

const tbody = document.getElementById("tbody");

const btn = document.getElementById("btn");

const nodes = [];
const render = (oldData, data) => {
  const oldDataLength = oldData.length;
  const newDataLength = data.length;
  if (oldDataLength && !newDataLength) {
    tbody.textContent = "";
    return;
  }
  if (!oldDataLength && newDataLength) {
    for (let i = 0; i < newDataLength; i++) {
      const item = data[i];
      const { id, label } = item;
      const tr = document.createElement("tr");
      const td1 = document.createElement("td");
      const td2 = document.createElement("td");
      const td3 = document.createElement("td");
      const input = document.createElement("input");
      td1.textContent = id;
      td2.textContent = label;
      td3.append(input);
      tr.append(td1);
      tr.append(td2);
      tr.append(td3);
      // nodes.push({
      //   key: id,
      //   node: tr,
      // });
      nodes.push(tr);
      tbody.append(tr);
    }
  }
  if (oldDataLength && newDataLength) {
    if (oldDataLength > newDataLength) {
      const diffrence = oldDataLength - newDataLength;
      for (let i = 0; i < diffrence; i++) {
        const node = nodes[nodes.length - 1];
        node.remove();
        nodes.pop();
      }
      for (let i = 0; i < newDataLength; i++) {
        const tds = nodes[i].querySelectorAll("td");
        const itemData = data[i];
        const { id, label } = itemData;
        tds[0].textContent = id;
        tds[1].textContent = label;
      }
      // for(let i = 0; i < oldDataLength; i++){
      //   const nodeObj = nodes[i];
      //   const isIncludes = data.some((e)=>e.id === nodeObj.key);
      //   if(isIncludes){
      //     ...
      //   }else{
      //     ...
      //   }
      // }
    }
  }
};

const clickHandler = () => {
  // Допустим, по клику отправляется запрос на сервер
  // Можно сделать через Promise, но смысла в этом особо нету
  render(data, newData); // старый массив с данными, новый массив с данными
};

render([], data); // изначальный массив, новый массив с данными

btn.addEventListener("click", clickHandler);

In this case, initially 3 objects would come from the server, which had the property id. For them, 3 lines are created and shown on the site:

1. The result in the second iteration (the first empty array).

1. The result in the second iteration (the first empty array).

Let's say we enter 1 to 3 respectively into each input:

2. The result of filling the inputs with data.

2. Filling result input'ov data.

In this case, in the DOM state we have for input is stored valuewhich is equal to the unique identifier respectively. If the implementation does not use the concept of “keys”, then when deleting an object with id equal to 2 the following result will be obtained:

3. Result of 3 iterations, when 2 objects come from the server.

3. Result of the 3rd iteration, when 2 objects come from the server.

In this case, the algorithm will compare only by the length of the arrays. If we had 3 elements, and now there are 2, then we need to remove 1 element from the end. If it were so that we had 3 elements, and 1000 elements came from the server, then we would need to add another 997 elements. If the length of the old array is equal to the length of the new one, then we would reuse all the same DOM nodes.

But what if, for example, instead of binding not only to DOM nodes, but also to data, we tested the code (subject to the algorithm being improved):

nodes.push({
  key: id,
  node: tr,
});
//nodes.push(tr);

then the result would be the following:

4. Result of 3 iterations (if we use the concept "keys").

4. Result of 3 iterations (if we use the concept of “keys”).

This result is the main difference between a key implementation and a non-key implementation. In one case, we don’t care which DOM node is used to display the data, and in the other, we save the node that corresponds to a certain key. Thus, we save not only the state of the application, which we, for example, store in some state manager, but we also save event handlers, values ​​in input'ah, basically, we save the DOM state of the node. This could be very useful if we had, say, a fillable scale for each list item, and we didn't have to fill it again. This also applies to Shadow DOM.

Subtleties of the algorithm

The so-called “cycle” algorithm has many situations that need to be taken into account. They all come from the peculiarities of working with DOM nodes and arrays. In an array, when comparing elements, we can check whether an element has been added, removed, or replaced with another. There may be situations when data comes from the server that is almost completely different from what was there. Let's say if we take an implementation with a ready-made algorithm, then on the first iteration we will make 10 consecutive elements, and on the second we will indicate a completely chaotic array:

const oldData = [
  {
    id: 1,
    label: "Текст 1",
  },
  {
    id: 2,
    label: "Текст 2",
  },
  {
    id: 3,
    label: "Текст 3",
  },
  {
    id: 4,
    label: "Текст 4",
  },
  {
    id: 5,
    label: "Текст 5",
  },
  {
    id: 6,
    label: "Текст 6",
  },
  {
    id: 7,
    label: "Текст 7",
  },
  {
    id: 8,
    label: "Текст 8",
  },
  {
    id: 9,
    label: "Текст 9",
  },
  {
    id: 10,
    label: "Текст 10",
  },
];

const newData = [
  {
    id: 1,
    label: "Текст 1",
  },
  {
    id: 6,
    label: "Текст 2",
  },
  {
    id: 18,
    label: "Текст 3",
  },
  {
    id: 3,
    label: "Текст 4",
  },
  {
    id: 7,
    label: "Текст 5",
  },
  {
    id: 9,
    label: "Текст 6",
  },
  {
    id: 8,
    label: "Текст 7",
  },
  {
    id: 4,
    label: "Текст 8",
  },
  {
    id: 2,
    label: "Текст 9",
  },
  {
    id: 13,
    label: "Текст 10",
  },
];

In this case, the algorithm must take into account all the subtleties that come with this implementation. The result of the work will be approximately the following:

5. Result with old data.

5. Result with old data.

After installing new data:

. 6. Result with new data.

. 6. Result with new data.

Thus, if you want to create a similar algorithm, you should take these points into account.

Conclusion

Key implementation is a standard for developing modern user interfaces. Almost all JavaScript (and not only) frameworks or libraries use this concept, because it allows you to save the state of the DOM node, which means that you do not have to re-enter the data that was entered on the client. Many events on the site are difficult to make controllable (scroll, form submission, click on an element), so “keys”, in this case, simplify the process of creating and using the site.

Thank you all for reading this article! In fact, it seems to be a simple topic, but “under the hood” it contains a lot of interesting subtleties that are worth knowing. When preparing the material, it was possible to also tell about the algorithm, but since this is already mathematics, then, in general, it was better to focus on the essence.

Sources

Camp Code – https://github.com/krausest/js-framework-benchmark/blob/master/frameworks/keyed/cample/src/main.js

Vue Code – https://github.com/krausest/js-framework-benchmark/blob/master/frameworks/keyed/vue/src/App.vue#L137

Old article – https://habr.com/ru/articles/751316

Video – https://youtu.be/_R5JjRP9c5k

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *