Lekcja 9: Listy i Klucze (Keys)
Bardzo często w aplikacjach webowych musimy wyświetlać listy danych - listę produktów, komentarzy, użytkowników itp. React pozwala na łatwe renderowanie list elementów za pomocą standardowych metod JavaScript, takich jak map()
, ale wymaga specjalnego traktowania w postaci kluczy (keys), aby działać wydajnie.
Renderowanie List za pomocą `map()`
Metoda map()
tablicy jest idealna do przekształcania listy danych (np. tablicy obiektów) w listę elementów React.
import React from \'react\';
function NumberList({ numbers }) {
// Przekształcamy tablicę liczb na tablicę elementów <li>
const listItems = numbers.map((number) =>
// WAŻNE: Dodajemy atrybut \'key\'!
<li key={number.toString()}>
{number}
</li>
);
return (
<ul>
{listItems}
</ul>
);
}
function App() {
const numbers = [1, 2, 3, 4, 5];
return <NumberList numbers={numbers} />;
}
export default App;
W powyższym przykładzie, dla każdej liczby w tablicy numbers
tworzymy element <li>
. Wynikowa tablica elementów listItems
jest następnie osadzana wewnątrz <ul>
.
Klucze (Keys)
Zauważ atrybut key={number.toString()}
w elemencie <li>
. Klucze są niezbędne, gdy renderujemy listy elementów. Pomagają one Reactowi zidentyfikować, które elementy listy zostały zmienione, dodane lub usunięte. Dzięki kluczom React może efektywnie aktualizować DOM, zamiast renderować całą listę od nowa przy każdej zmianie.
Wymagania dotyczące kluczy:
- Unikalność wśród rodzeństwa: Klucze muszą być unikalne tylko wśród bezpośrednich elementów rodzeństwa w danej liście. Nie muszą być globalnie unikalne w całej aplikacji.
- Stabilność: Klucz dla danego elementu nie powinien się zmieniać między kolejnymi renderowaniami. Używanie indeksu tablicy jako klucza (
key={index}
) jest zazwyczaj złym pomysłem, jeśli kolejność elementów może się zmieniać (np. przez sortowanie, dodawanie/usuwanie elementów na początku/w środku listy). Może to prowadzić do problemów z wydajnością i błędów w stanie komponentów. - Typ: Klucze powinny być stringami lub liczbami.
Najlepszym źródłem kluczy są unikalne identyfikatory (ID) pochodzące z danych, np. ID produktu, ID użytkownika z bazy danych.
function TodoList({ todos }) {
const todoItems = todos.map((todo) =>
// Używamy \'todo.id\' jako stabilnego i unikalnego klucza
<li key={todo.id}>
{todo.text}
</li>
);
return <ul>{todoItems}</ul>;
}
const initialTodos = [
{ id: \'a\', text: \'Nauczyć się Reacta\' },
{ id: \'b\', text: \'Zrobić zakupy\' },
{ id: \'c\', text: \'Pójść na spacer\' }
];
// Użycie: <TodoList todos={initialTodos} />
Co jeśli nie mam stabilnych ID?
Jeśli dane nie posiadają unikalnych i stabilnych ID, jako ostateczność można użyć indeksu tablicy (key={index}
). Jest to jednak bezpieczne tylko wtedy, gdy:
- Lista i jej elementy są statyczne - nigdy nie będą zmieniane ani sortowane.
- Elementy listy nie mają własnego stanu.
- Lista nigdy nie będzie filtrowana ani nie będą dodawane/usuwane elementy w środku/na początku.
W przeciwnym razie, użycie indeksu jako klucza może prowadzić do błędów i problemów z wydajnością. Jeśli nie masz ID, rozważ dodanie ich do danych (np. za pomocą biblioteki generującej UUID) przed renderowaniem.
Ekstrakcja Komponentów z Listy
Często elementy listy są na tyle złożone, że warto wydzielić je do osobnego komponentu. Klucz nadal powinien być przypisany do elementu komponentu wewnątrz metody map()
, a nie wewnątrz samego komponentu elementu listy.
// Komponent dla pojedynczego elementu listy
function ListItem({ value }) {
// Klucz nie jest potrzebny tutaj!
return <li>{value}</li>;
}
function NumberListWithComponent({ numbers }) {
const listItems = numbers.map((number) =>
// Klucz musi być przypisany tutaj, wewnątrz map()
<ListItem key={number.toString()} value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
Ważne: Klucze służą jako wskazówka dla Reacta, ale nie są przekazywane jako props do komponentu (nie można uzyskać dostępu do props.key
wewnątrz komponentu ListItem
). Jeśli potrzebujesz tej samej wartości wewnątrz komponentu, musisz ją przekazać jako osobny prop (np. <ListItem key={item.id} id={item.id} value={item.text} />
).
Osadzanie `map()` bezpośrednio w JSX
Zamiast tworzyć osobną zmienną (jak listItems
), często wygodniej jest osadzić wywołanie map()
bezpośrednio w JSX:
function SimpleTodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}
</li>
))}
</ul>
);
}
Ćwiczenie praktyczne
Pokaż rozwiązanie
// src/User.jsx
import React from \'react\';
function User({ user }) {
// Klucz nie jest potrzebny tutaj, jest używany w map()
return (
<li style={{ border: \'1px solid #ccc\', margin: \'5px\', padding: \'5px\' }}>
<strong>{user.name}</strong> ({user.email})
</li>
);
}
export default User;
// src/UserList.jsx
import React from \'react\';
import User from \'./User\";
function UserList({ users }) {
return (
<ul style={{ listStyle: \'none\', padding: 0 }}>
{users.map(user => (
// Klucz jest niezbędny tutaj!
<User key={user.id} user={user} />
))}
</ul>
);
}
export default UserList;
// src/App.jsx
import React from \'react\';
import UserList from \'./UserList\";
const initialUsers = [
{ id: 1, name: \'Alicja\', email: \'alicja@example.com\' },
{ id: 2, name: \'Bartosz\', email: \'bartosz@example.com\' },
{ id: 3, name: \'Celina\', email: \'celina@example.com\' }
];
function App() {
return (
<div>
<h1>Lista Użytkowników</h1>
<UserList users={initialUsers} />
</div>
);
}
export default App;
Cel: Wyrenderować listę użytkowników, gdzie każdy użytkownik jest osobnym komponentem.
Kroki:
- Zdefiniuj tablicę obiektów reprezentujących użytkowników, np.:
const users = [ { id: 1, name: \'Alicja\', email: \'alicja@example.com\' }, { id: 2, name: \'Bartosz\', email: \'bartosz@example.com\' }, { id: 3, name: \'Celina\', email: \'celina@example.com\' } ];
- Stwórz komponent funkcyjny
User
, który przyjmuje propsuser
(obiekt użytkownika) i wyświetla jego imię oraz email w elemencie<div>
lub<li>
. - Stwórz komponent funkcyjny
UserList
, który przyjmuje propsusers
(tablicę użytkowników). - W komponencie
UserList
, użyj metodymap()
na tablicyusers
, aby dla każdego użytkownika wyrenderować komponentUser
. - Pamiętaj o dodaniu unikalnego i stabilnego atrybutu
key
(użyjuser.id
) do każdego komponentuUser
wewnątrzmap()
. - Zwróć listę komponentów
User
opakowaną w<div>
lub<ul>
. - Użyj komponentu
UserList
wApp.jsx
, przekazując tablicę użytkowników.
Zadanie do samodzielnego wykonania
Stwórz komponent ProductList
, który renderuje listę produktów. Każdy produkt powinien mieć id
, name
i price
.
- Zdefiniuj tablicę przykładowych produktów.
- Stwórz komponent
Product
, który wyświetla nazwę i cenę produktu. - W komponencie
ProductList
użyjmap()
do wyrenderowania komponentówProduct
dla każdego produktu w tablicy. - Upewnij się, że używasz
product.id
jako klucza. - Dodaj przycisk "Sortuj wg ceny", który po kliknięciu sortuje tablicę produktów (przechowywaną w stanie komponentu
App
lubProductList
) i powoduje ponowne renderowanie listy w nowej kolejności. Zaobserwuj, jak React efektywnie aktualizuje listę dzięki kluczom.
FAQ - Listy i Klucze (Keys)
Dlaczego klucze są tak ważne?
Klucze pomagają Reactowi efektywnie identyfikować, które elementy w liście uległy zmianie, zostały dodane lub usunięte. Bez kluczy, React musiałby przy każdej zmianie porównywać całą strukturę listy, co jest nieefektywne, a w przypadku komponentów ze stanem może prowadzić do utraty stanu lub niepoprawnego jego przypisania.
Czy mogę użyć indeksu tablicy jako klucza (`key={index}`)?
Można, ale jest to zalecane tylko jako ostateczność i tylko wtedy, gdy lista jest statyczna (nie zmienia kolejności, nie jest filtrowana, elementy nie są dodawane/usuwane w środku). Jeśli kolejność elementów może się zmienić, użycie indeksu jako klucza może prowadzić do błędów i problemów z wydajnością.
Co się stanie, jeśli nie podam kluczy?
React wyświetli ostrzeżenie w konsoli deweloperskiej. Chociaż aplikacja może pozornie działać, brak kluczy może prowadzić do problemów z wydajnością i nieprzewidywalnego zachowania, zwłaszcza gdy lista jest dynamicznie modyfikowana lub zawiera komponenty ze stanem.
Czy klucze muszą być unikalne globalnie?
Nie, klucze muszą być unikalne tylko wśród bezpośrednich elementów rodzeństwa w tej samej tablicy (wewnątrz jednego wywołania `map()`). Ten sam klucz może być użyty w innej liście lub na innym poziomie zagnieżdżenia.
Gdzie dokładnie powinienem umieścić atrybut `key`?
Atrybut `key` należy umieścić na najwyższym elemencie zwracanym wewnątrz wywołania `map()`. Jeśli `map()` zwraca bezpośrednio elementy JSX (np. `<li>`), klucz idzie na `<li>`. Jeśli `map()` zwraca komponent (np. `<MyItem />`), klucz idzie na `<MyItem />`.
Czy `key` jest przekazywany jako props do komponentu?
Nie, `key` jest specjalnym atrybutem używanym wewnętrznie przez React i nie jest dostępny jako `props.key` wewnątrz komponentu. Jeśli potrzebujesz tej wartości w komponencie, musisz ją przekazać jako osobny, zwykły prop, np. `id={item.id}`.
Czy mogę używać `Fragment`ów w listach? Czy potrzebują kluczy?
Tak, można używać `Fragment`ów w listach. Jeśli używasz składni `<React.Fragment>`, musisz dodać do niej klucz: `<React.Fragment key={item.id}>...</React.Fragment>`. Krótsza składnia `<>` nie pozwala na dodanie klucza, więc w listach należy używać pełnej składni `<React.Fragment key={...}>`.