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:

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:

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:

  1. 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\' }
    ];
  2. Stwórz komponent funkcyjny User, który przyjmuje props user (obiekt użytkownika) i wyświetla jego imię oraz email w elemencie <div> lub <li>.
  3. Stwórz komponent funkcyjny UserList, który przyjmuje props users (tablicę użytkowników).
  4. W komponencie UserList, użyj metody map() na tablicy users, aby dla każdego użytkownika wyrenderować komponent User.
  5. Pamiętaj o dodaniu unikalnego i stabilnego atrybutu key (użyj user.id) do każdego komponentu User wewnątrz map().
  6. Zwróć listę komponentów User opakowaną w <div> lub <ul>.
  7. Użyj komponentu UserList w App.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.

  1. Zdefiniuj tablicę przykładowych produktów.
  2. Stwórz komponent Product, który wyświetla nazwę i cenę produktu.
  3. W komponencie ProductList użyj map() do wyrenderowania komponentów Product dla każdego produktu w tablicy.
  4. Upewnij się, że używasz product.id jako klucza.
  5. Dodaj przycisk "Sortuj wg ceny", który po kliknięciu sortuje tablicę produktów (przechowywaną w stanie komponentu App lub ProductList) 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={...}>`.