Strona główna / Artykuły

Klucze w React jako tożsamość komponentu: zachowywanie i resetowanie stanu

Klucze React decydują, która instancja komponentu przeżyje render. Jak klucze indeksowe przeciekają stan między wierszami oraz jak zmiana klucza celowo resetuje formularz.

Wydawca: REACTAPP.TOP6 min czytania1462 słów

Źródła: React Keys Solve a Much Stranger Problem Than Rendering Lists

Większość programistów używających React dodaje key do każdej wywołania .map() bez zastanowienia, a większość wyjaśnień kończy się stwierdzeniem „React potrzebuje go, aby śledzić, które elementy się zmieniły”. To odpowiedź prawidłowa, ale niekompletna. Rzeczywiste pytanie, które React zadaje przy każdym renderowaniu, brzmi: czy element, który teraz widzi, to ten sam co ostatnio, a key służy do odpowiedzi na to pytanie. Gdy potraktujesz key jako mechanizm identyfikacji, błędy związane z użyciem indeksu jako klucza stają się przewidywalne, a ty uzyskujesz czysty, deklaratywny sposób na resetowanie stanu komponentu według potrzeb.

wersja refleksywna

Oto wzorzec, który występuje w niemal każdym kodzie. Każdy użytkownik z tablicy staje się elementem User, a jego id służy jako klucz:

users.map((user) => (
  <User key={user.id} user={user} />
))

W tym fragmencie nie ma nic złego. Interesująca jest sytuacja, gdy klucz zostanie źle wybrany – problem pojawia się dopiero wtedy, gdy komponenty przechowują własny stan.

Komponent z stanem ułatwia dostrzeżenie problemu

Rozważmy komponent karty, który przechowuje lokalną flagę selected. Flaga ta znajduje się wewnątrz komponentu, a nie w propozycji user, więc React musi decydować, która instancja karty posiada daną flagę podczas kolejnych renderów:

interface UserCardProps {
  user: User;
}

const UserCard = ({ user }: UserCardProps) => {
  const [selected, setSelected] = useState(false);

  return (
    // ...
  );
}

Teraz wyświetlmy trzy osoby: Alice, Boba oraz trzeciego użytkownika. Tablica zapisana jako pary indeks–element wygląda w ten sposób:

0 → Alice
1 → Bob
2 → Maxime

To, co widzi React przy użyciu kluczy indeksowych

Powszechną zasadą jest unikanie indeksów tablicy jako kluczy. Powód tego staje się jasny, gdy przyjrzymy się elementom, które faktycznie otrzymuje React po użyciu indeksu:

<User key={0} user={alice} />
<User key={1} user={bob} />
<User key={2} user={maxime} />

Załóżmy, że lista umożliwia usuwanie, dodawanie lub przestawianie elementów poprzez przeciąganie i upuszczanie. Usunięcie Alice powoduje, że przy następnej renderizacji otrzymujemy coś takiego:

<User key={0} user={bob} />
<User key={1} user={maxime} />

Z twojej perspektywy Alice zniknęła, a pozostałe dwie osoby przesunęły się wyżej. Bob przeniósł się z indeksu 1 na 0, a trzeci użytkownik z 2 na 1. Z punktu widzenia React nic takiego nie wskazuje – widzi on, że element o key={0} istniał wcześniej i nadal istnieje, przy tym z tą samą typem komponentu. Ten sam typ w połączeniu z tym samym kluczem to dokładnie sygnał, którym React decyduje, czy dana instancja powinna zostać zachowana. Dlatego zachowuje instancję, która wcześniej renderowała Alice i przekazuje jej właściwości Boba, zachowuje drugą instancję i przekazuje jej właściwości trzeciego użytkownika, a trzecią deinstaluje.

Skutek: jeśli wybrano kartę Alice, karta Boba teraz wygląda na wybraną, ponieważ stan selected należał do instancji o kluczu 0, a nie do Alice. Ten sam rodzaj wycieku dotyka niekontrolowanych danych wejściowych, fokusa, pozycji przewijania wewnątrz wiersza oraz wszelkich innych stanów związanych z instancją.

Dlaczego te błędy przeciekają

Nieprawidłowe klucze są zaskakująco powszechne w rzeczywistych aplikacjach o każdej wielkości. Lista, do której dodawane są tylko nowe elementy i która nigdy nie zmienia kolejności, zachowuje się identycznie z kluczami indeksowymi i stabilnymi, więc nic nie wygląda na nieprawidłowe. Błąd ujawnia się później, gdy dodaje się funkcje usuwania, filtrowania, sortowania lub przestawiania elementów. Narzędzia do analizy kodu pomagają: reguła react/no-array-index-key z eslint-plugin-react wskazuje na klucze indeksowe, dzięki czemu decyzja jest podejmowana świadomie, a nie z przyzwyczajenia.

Gdy element rzeczywiście nie ma stabilnego identyfikatora, należy go wygenerować w momencie tworzenia danych (na przykład za pomocą crypto.randomUUID() w chwili dodawania), a nie podczas renderowania. Klucz wygenerowany wewnątrz .map() zmienia się przy każdym renderowaniu i zmusza React do ponownego utworzenia każdego wiersza, co jest gorsze niż używanie indeksu.

Props opisują treść, klucze opisują tożsamość

Tutaj pomocny jest prosty model myślowy:

  • props odpowiadają na pytanie „co aktualnie zawiera ten element?”
  • klucz odpowiada na pytanie „o który element chodzi?”

Props mogą swobodnie się zmieniać, podczas gdy komponent pozostaje tą samą instancją ze zachowanym stanem. Jeśli wiersz umożliwia edycję bezpośrednią, zmiana imienia użytkownika nie powinna sprawić, że React usunie ten wiersz i zacznie od nowa – to nadal ten sam użytkownik. Stabilny klucz, taki jak user.id, dokładnie to wyraża.

Celowe wymuszenie utworzenia nowej instancji

Czasami chcemy czegoś przeciwnego: chcemy, aby React traktował komponent jak zupełnie nowy. Klucz może być również użyty poza listami:

<Profile key={user.id} user={user} />

Załóżmy, że Profile przechowuje niesoхранiony szkic w lokalnym stanie:

interface ProfileProps {
  user: User;
}

const Profile = ({ user }: ProfileProps) => {
  const [draft, setDraft] = useState({...});
}

Jeśli rodzic zmieni użytkownika, przeniesienie szkicu poprzedniego użytkownika do nowego profilu stanowiłoby poważny błąd, a nawet wyciek danych. Bez klucza React widzi ten sam typ komponentu na tej samej pozycji i zachowuje instancję, więc szkic przetrwa. Dzięki key={user.id} nowy identyfikator informuje React, że stary Profile już nie istnieje. React rozmontowuje starą instancję, usuwa jej stan, wykona czyszczenie efektów i montuje nową, w której inicjalizatory useState są uruchamiane ponownie.

To jest idiomatyczna alternatywa dla pisania efektu, który obserwuje user i ręcznie resetuje każdą wartość stanu. Taki podejście renderuje formularz raz z przestarzałym stanem, zanim go skoryguje, a łatwo jest zapomnieć o jakimś polu. Aby dowiedzieć się więcej na temat tego, dlaczego synchronizacja stanu za pomocą efektów często przynosi negatywne skutki, zapoznaj się z bezpieczniejszym wzorcem zastępującym synchronizację opartą na useEffect.

Resetowanie formularza za pomocą klucza

To samo rozwiązanie umożliwia czysty reset formularzy:

<CreateUserForm key={draft.id} />

Klucz pełni rolę granicy identyfikacyjnej. Ilekroć zmienia się draft.id, na przykład po udanej wysyłce, która tworzy nowy projekt, React buduje nowy obiekt CreateUserForm bez pozostałych wartości pól, błędów walidacji czy flag wskazujących na edycję.

Należy pamiętać o dwóch kwestiach. Ponowne zamontowanie służy do resetowania całego poddrzewa, włączając dzieci i wszystko, co zostało pobrane podczas pierwszego zamontowania, więc należy go używać w komponentach, gdzie chodzi o całkowite ponowne uruchomienie. Ponadto wymaga pełnego odmontowania i zamontowania, co jest w porządku dla formularza, ale nie powinno być uruchamiane przy każdym naciśnięciu klawisza.

Gdzie naprawdę znajduje się stan

Wszystko to wynika z sposobu, w jaki React przechowuje stan. Stan nie jest powiązany z funkcją, którą napisałeś; gdyby tak było, każdy element <User /> dzieliłby się tym samym wartością selected. Zamiast tego React łączy stan z konkretną pozycją w drzewie renderowanym. Przy każdym renderowaniu przegląda nowy wynik i dla każdego elementu decyduje, czy stanowi on kontynuację czegoś z poprzedniego renderowania. Typ komponentu i jego pozycja biorą udział w tej decyzji, a klucz ułatwia dokładniejszą identyfikację wśród elementów spokrewnionych. Artykuł na temat gdzie useState zachowuje swoją wartość zagłębia się w strukturę fiber leżącą u podstaw tego mechanizmu.

Zasada, która z tego wynika, jest prosta:

  • ten sam typ, ta sama pozycja, ten sam klucz: React traktuje to jako ten sam komponent i zachowuje jego stan
  • inny typ lub inna kluczowa wartość: React odrzuca stary stan i tworzy nową instancję
  • Listy to po prostu pierwsze miejsce, gdzie pozycja przestaje być wiarygodna. Gdy elementy mogą się przesuwać, pojawiać lub znikać, „drugi element” nie oznacza już „tego samego co ostatnio”, dlatego React wymaga podania identyfikatora. Stabilna kluczowa wartość umożliwia mu śledzenie elementu w miarę zmiany listy; zmieniona kluczowa wartość jest wyraźnym sygnałem, że jej miejsce zajął coś nowego.

    Główne wnioski

    • Kluczowa wartość informuje React, o której instancji elementu chodzi, a nie tylko o tym, który element się zmienił.
    • Klucze oparte na indeksach łączą stan z konkretnym miejscem w liście, więc usuwanie lub przestawianie elementów przenosi stan na niewłaściwe pozycje.
    • Używaj identyfikatorów, które pozostają stabilne podczas różnych renderowań; nigdy nie generuj kluczowych wartości w trakcie renderowania.
    • Zamiana klucza celowo to najczystszy sposób na sformatowanie komponentu, takiego jak formularz lub edytor dla poszczególnych obiektów.
    • Sformatowanie za pomocą klucza stosuj rzadko: powoduje ponowne załadowanie całego poddrzewa i usuwa wszystko, co się w nim znajduje.

    Literatura pokrewna