Към съдържанието
wannacode

Списъци и ключове

Накратко: Списъкът в React се прави с map от масив към компоненти, а key казва на React кой стар ред е кой нов; случайното число е лош ключ, а индексът е лош, щом списъкът се мени.

Проблем

Махнах хляба, а млякото стана 3

Купувачът има в кошницата хляб, мляко и сирене. Пише 3 при хляба и 2 при млякото, а после маха хляба. Пробвай: смени двете числа и натисни „Махни“ при хляба.

React

Хлябът е махнат, но млякото вече е 3, а сиренето е 2. В конзолата няма грешка. Данните са верни: махнат е точно хлябът. Объркани са само полетата. Причината е един атрибут на ред 20 в App.jsx: какво казва кодът на библиотеката за всеки ред от списъка.

Идеята

Списък в компонент

В DOM модула списъкът се правеше от масив с map и createElement. В компонент е същата идея, но всеки елемент от масива става част от разметката:

<ul>
{items.map((p) => (
<li key={p.id}>
<ProductCard product={p} />
</li>
))}
</ul>

map връща масив от JSX елементи, а React ги рисува един след друг. Всеки елемент от масива има атрибут key: ключ, по който React го разпознава.

Когато данните се сменят, компонентът връща нов списък. Библиотеката не трие стария, за да го нарисува наново. Тя сравнява стария и новия и сменя в страницата само разликата. Затова фокусът и написаното в полетата остават, а не се губят като при render() от DOM модула.

За да сравни, тя трябва да знае кой стар ред на кой нов отговаря. Това казва ключът.

Идеята

Сдвояване по ключ

Библиотеката сдвоява старите и новите редове по ключ:

  • Същият ключ е същият ред. Елементът в страницата остава, а библиотеката сменя в него само каквото е различно в данните: името, цената.
  • Ключ, който го няма в новия списък, е махнат ред. Елементът му се маха от страницата.
  • Нов ключ е нов ред. Прави се нов елемент.

Елементът, който остава, пази всичко свое: написаното в <input>, фокуса, отвореното падащо меню. Тези неща не са в данните, а в самия елемент. Затова е важно той да остане при своя продукт.

В примера ключът е индексът: мястото в масива. След „Махни“ млякото е на място 0. Старият ред с ключ 0 беше на хляба. Библиотеката го запазва, сменя името му на „Прясно мляко“ и оставя полето с 3.

Виж го

Виж сдвояването

Отляво е страницата преди „Махни“, отдясно е новият списък от компонента. Гледай кой ред се маха.

// key = мястото в списъка
{items.map((p, index) => (
<li key={index}>
<ProductCard product={p} />
</li>
))}
// key = id на продукта
{items.map((p) => (
<li key={p.id}>
<ProductCard product={p} />
</li>
))}
Стъпка по стъпка, с думи
  1. Три реда в кошницата, а ключът е мястото: 0, 1, 2. Купувачът е написал 3, 2 и 1 в полетата. Написаното живее в елемента <input>, а не в данните.
  2. „Махни Бял хляб“. В данните вече има два продукта и компонентът връща нов списък: ключ 0 е млякото, ключ 1 е сиренето.
  3. Библиотеката сдвоява стария и новия списък по ключ: 0 с 0, 1 с 1. Ключ 2 го няма в новия, затова се маха третият ред: този на сиренето.
  4. Запазените редове получават новите данни и заглавията им вече са мляко и сирене. Полетата са старите, с 3 и 2. Броят на хляба е при млякото.
  5. Същата кошница с ключ id. Ключовете са 1, 2, 3 и са на продукта, а не на мястото.
  6. Пак „Махни Бял хляб“. Новият списък е с ключове 2 и 3.
  7. Ключ 2 се сдвоява с 2, а 3 с 3, макар че са се преместили нагоре. Ключ 1 липсва и се маха редът на хляба, заедно с полето му.
  8. Всяко поле е останало при своя продукт: мляко 2, сирене 1. Ключът казва на библиотеката кой кой е, а не къде е.

С индекса се маха последният ред, а първите два сменят имената си. С id се маха редът на хляба, а другите два само се качват нагоре с полетата си. И в двата случая данните са едни и същи. Разликата е какво казва ключът: мястото или продукта.

Пробвай

Поправи ключа

Поправи кошницата така, че след „Махни“ всяко число да остане при своя продукт. Проверката пише 3 при хляба и 2 при млякото, маха хляба и гледа полетата на млякото и сиренето.

React

Пробвай

Попълни

Как се казва атрибутът, по който библиотеката разпознава реда?

{items.map((p) => (
  <li ={p.id}>
    <ProductCard product={p} />
  </li>
))}

Идеята

Добрият ключ

Добрият ключ е стабилен и уникален: един и същ за един и същ продукт при всяко рисуване и различен за различните продукти. Най-добрият е id от данните: той идва от базата и не се мени.

  • Индексът е лош ключ, ако списъкът се мени: махане, добавяне в началото, подреждане, филтър. Става, ако списъкът е само за четене и никога не мени реда си.
  • Math.random() е лош ключ винаги. При всяко рисуване ключът е нов, затова нито един стар ред няма двойка. Библиотеката маха всички редове и прави нови. Полетата губят написаното, а фокусът изчезва.
  • Два еднакви ключа объркват сдвояването. Тук ключът е името, а „Кисело мляко“ има в два размера. Натисни „Подреди по цена“ и брой редовете.
React

Продуктите са четири, а редовете стават пет: едното кисело мляко е два пъти.

React казва това още при първото рисуване, с грешка в конзолата: „Encountered two children with the same key, Кисело мляко“. Ако забравиш ключа, грешката е „Each child in a list should have a unique "key" prop.“ Двете съобщения са от React в режим за разработка. В истинския сайт конзолата мълчи, а грешката в страницата остава.

Пробвай

Намери грешката

При всяко рисуване наново всички полета „Брой“ се връщат на 1, а курсорът излиза от полето.

Избери реда с грешката.

Идеята

Филтър и подредба

Каталогът показва само наличните продукти, подредени по цена. Това е изведен списък: не се пази отделно, а се прави от данните при всяко рисуване.

const visible = PRODUCTS
.filter((p) => p.stock > 0)
.sort((a, b) => a.price - b.price);

Това е в тялото на компонента: всяко рисуване прави visible наново от PRODUCTS.

  • filter връща нов масив само с наличните. Оригиналът остава цял.
  • sort подрежда масива на място. Тук е безопасно, защото е след filter, а той вече е дал копие. Без филтър първо се прави копие: [...PRODUCTS].sort(…).
  • Ключът остава p.id. При подреждане редовете сменят местата си, а индексът би сменил ключа на всеки от тях.

Пробвай

Само наличните

Покажи само продуктите в наличност (stock > 0), подредени по цена от най-евтиния. Над списъка напиши „Налични: 3 от 5“.

React
По-точно, за любопитните

key не е prop. Библиотеката взима ключа за себе си и не го подава на компонента. props.key в ProductCard е undefined, а React пише грешка „key is not a prop“. Ако компонентът има нужда от id, подай го отделно: product вече го съдържа.

Ключ за нулиране. Нов ключ значи нов елемент. Затова сменен ключ е начин да нулираш компонент: формуляр с ключ id на продукта започва празен при всеки нов продукт.

Няколко елемента на ред. Ако един продукт е два елемента (<dt> и <dd>), ключът е на обвивката: <Fragment key={p.id}>…</Fragment>. Краткият запис <>…</> не приема ключ.

Класовите компоненти. Старият код на React има компоненти като класове (class ProductList extends Component). Ключовете в тях работят по същия начин.

toSorted. products.toSorted((a, b) => a.price - b.price) връща подредено копие и не пипа оригинала. Той е в браузърите от 2023 г.

Защо полето е „неконтролирано“. Тук числото в полето е само в DOM-а. В модула „Състояние“ полето става контролирано: числото е в данните и ги следва. Тогава ключът пак е важен: за фокуса, за курсора и за състоянието, което компонентът пази в себе си.

Упражнение

Кошницата

Състоянието и бутонът „Подреди по цена“ са готови. Направи реда за всеки продукт: ProductCard и бутон „Махни“ с име за екранния четец „Махни Бял хляб“. Проверката пише бройки, подрежда, маха хляба и гледа, че всяко число остава при своя продукт.

React

Тест

Провери се

1. По какво библиотеката разбира кой ред от стария списък е кой ред от новия?
2. Ключът е индексът. Купувачът маха първия продукт. Какво става с полетата на останалите редове?
3. Защо Math.random() е лош ключ?
4. Списъкът трябва да е подреден по цена. Как?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·