Strona główna / Artykuły / Wskazówki praktyczne: Komponenty React muszą być czyste — sekret StrictMode

Wskazówki praktyczne: Komponenty React muszą być czyste — sekret StrictMode

Krok po kroku praktyczne wskazówki: Komponenty React muszą być czyste — sekret StrictMode: umowy, sprawdzania oraz miejsca na kod do wstawienia dla zespołów stosujących ten wzorzec.

1954 słów

Niech to służy jako przebudowa idei z artykułu „React Components Must Be Pure — the Secret Behind StrictMode and the Compiler” przeznaczona dla operatorów: wyraźne etapy, uporządkowane sekcje kodu oraz notatki naprawcze, które przetrwają przeniesienie obowiązków. Etap Przeglądu działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię do analizy. Zapisz jeden idealny przykład działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres pracy. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzucaj ciche, częściowe ukończenie zadań.

Czym jest funkcja czysta?

Dla etapu „Co to jest funkcja czysta” należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy rejestrować czasy wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega nieoczekiwanym rachunkom, gdy ścieżka przechodzi z środowiska demonstracyjnego do współdzielonych środowisk. Stan powinien być umieszczany obok komponentu, który odpowiada za jego modyfikację. Umieszczanie wszystkiego w globalnym magazynie utrudnia wykrycie błędów związanych z czasem wykonywania.

// ✅ pure function — same input, same output, and it touches nothing outside
function double(x: number): number {
  return x * 2;
}
double(3); // 6
double(3); // 6 — six no matter how many times you call it

// 🔴 impure function — it changes the outer variable `total` (a side effect)
let total = 0;
function addToTotal(x: number): number {
  total += x;     // side effect!
  return total;   // the result changes on every call
}
addToTotal(3); // 3
addToTotal(3); // 6 — same input, different result

Komponent również jest funkcją czystą

Dla komponentu A, który stanowi etap procesu, należy zdefiniować dane wejściowe, osobę odpowiedzialną za ten etap oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić ten etap od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Konfigurację należy przechowywać poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury aplikacji. Stan powinien być przechowywany razem z komponentem, który odpowiada za jego modyfikację. Umieszczanie wszystkiego w globalnym magazynie utrudnia wykrycie błędów związanych z czasem wykonywania operacji.

type Props = { name: string };

// ✅ pure — same name, always the same result
function Greeting({ name }: Props) {
  return <h1>Hello, {name}!</h1>;
}

Złamanie zasady — Nie ingerować w elementy zewnętrzne podczas renderowania

Dla etapu Break the Rule Don należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy udokumentować zarówno prawidłowy przebieg działania, jak i ścieżkę naprawczą. Próby ponownych działań, kontrolne punkty ludzkie oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dodawane później. Stan powinien być przechowywany razem z komponentem, który odpowiada za jego modyfikację. Umieszczanie wszystkiego w globalnym magazynie utrudnia wykrycie błędów związanych z czasem realizacji. Dla etapu Break the Rule Don należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nazwij poszczególne elementy, zdefiniuj kryteria sukcesu i odrzucaj ciche, częściowe ukończenie zadań.

let count = 0; // a variable outside the component

// 🔴 not pure — changes an external variable while rendering
function Counter() {
  count = count + 1; // side effect!
  return <p>{count}</p>;
}

Rozwiązaj to sam — dlaczego pojawia się „2”?

Gdy przechodzisz przez etap „Rozwiązaj to sam”, najpierw zapisz specyfikację: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Zapisz czasy wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega nieoczekiwanym rachunkom, gdy ścieżka przechodzi z środowiska demonstracyjnego do współdzielonych środowisk. Traktuj efekty jako synchronizację z otaczającym światem, a nie jako zamiennik wartości wyliczanych podczas renderowania.

import { useState } from 'react';

let count = 0;

function Counter() {
  const [, setTick] = useState(0); // a device to trigger re-renders
  count = count + 1; // 🔴 changes an external variable on every render
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setTick((n) => n + 1)}>Re-render</button>
    </div>
  );
}
// ✅ pure — computed from input (props) alone
function Counter({ count }: { count: number }) {
  return <p>{count}</p>;
}

Ale możesz zmienić „elementy utworzone podczas renderowania”

Gdy przechodzisz przez etap „But You Can Change”, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Traktuj efekty jako synchronizację z otaczającym światem, a nie jako zamiennik wartości pochodnych podczas renderowania.

function ProductList({ products }: { products: Product[] }) {
  // ✅ this array was just created in this render — handle it however you like
  const sorted = [...products].sort((a, b) => a.price - b.price);
  return (
    <ul>
      {sorted.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

Rozwiązano tajemnicę nr 1 — Dlaczego StrictMode uruchamia operacje dwa razy

Gdy przechodzisz przez etap Mystery 1 Solved Why, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Zdokumentuj zarówno ścieżkę prawidłowego działania, jak i ścieżkę naprawczą. Próby ponowne, kontrolne punkty ludzkie oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dodawane później. Traktuj efekty uboczne jako synchronizację z otaczającym światem, a nie jako zamiennik wartości wyliczanych podczas renderowania. Gdy przechodzisz przez etap Mystery 1 Solved Why, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzucaj ciche, częściowe ukończenie zadania.

// main.tsx — in development, the components inside this render twice
createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

A dokąd trafiają efekty uboczne?

Prace typu side stage działają najlepiej, gdy traktuje się je jako mierzalną powierzchnię. Zapisz jeden udany przypadek, jeden przypadek awarii oraz notatkę o cofnięciu zmian, zanim rozszerzysz zakres pracy. Zapisuj czasy wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega niespodziewanym rachunkom, gdy ścieżka przechodzi z wersji demonstracyjnej do środowisk współdzielonych. Utrzymuj koszty renderowania na niskim poziomie i odkładaj drogie operacje wywodzenia na później, dopiero po ich zmierzeniu. Przedwczesne stosowanie mechanizmów memoizacji może ukrywać błędy związane ze starymi danymi.

// 🔴 side effect during rendering — fires on every render
function ProductPage({ id }: { id: number }) {
  logView(id); // a side effect in the render body — not allowed
  return <h1>Product {id}</h1>;
}

// ✅ in an event handler — runs only at the moment the user clicks
function BuyButton({ id }: { id: number }) {
  return <button onClick={() => logPurchase(id)}>Buy</button>;
}

Rozwiązano tajemnicę nr 2 — Dlaczego kompilator React polega na czystości kodu

Tajemnica nr 2 rozwiązana: dlaczego scena działa najlepiej, gdy traktuje się ją jako mierzalną powierzchnię. Zapisz jeden idealny przepis, jeden przypadek awarii oraz notatkę o cofnięciu zmian, zanim rozszerzysz zakres pracy. Trzymaj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności przeglądania całej struktury. Utrzymuj koszty renderowania na niskim poziomie i odkładaj drogie operacje obliczeniowe na później, dopiero po ich zmierzeniu. Przedwczesne stosowanie mechanizmów memoizacji może ukrywać błędy związane ze starymi wartościami.

// 🔴 not pure → the compiler skips optimization (ESLint points at this component)
let renderCount = 0;
function RenderCounter() {
  renderCount++; // mutating an external variable during render — a violation
  return <p>{renderCount}</p>;
}

// ✅ pure → the compiler memoizes automatically (reuse the previous result when inputs match)
function Label({ text }: { text: string }) {
  return <p>{text}</p>;
}

function App() {
  return (
    <>
      <RenderCounter />
      <Label text="Pure Component" />
    </>
  );
}

export default App;

Rozpatrywanie interfejsu jako drzewa

Interfejs Seeing UI jako scena funkcjonuje najlepiej, gdy traktowany jest jako mierzalna powierzchnia. Zapisz jeden idealny przepis działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres. Zdokumentuj zarówno prawidłowy przebieg działania, jak i ścieżkę naprawczą. Próby ponownych działań, kontrolne punkty ludzkie oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dodawane później. Utrzymuj koszty renderowania na niskim poziomie i odkładaj drogie operacje wywodzenia na później, tylko po dokonaniu pomiarów. Przedwczesne stosowanie mechanizmów memoizacji może ukrywać błędy związane ze starymi danymi. Interfejs Seeing UI jako scena funkcjonuje najlepiej, gdy traktowany jest jako mierzalna powierzchnia. Zapisz jeden idealny przepis działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres. Traktuj tę fazę jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nazwij poszczególne elementy, zdefiniuj kryteria sukcesu i odrzucaj ciche, częściowe ukończenie zadań.

function App() {
  return <ProductList products={products} />;
}

function ProductList({ products }: { products: Product[] }) {
  return (
    <ul>
      {products.map((p) => (
        <ProductCard key={p.id} product={p} />
      ))}
    </ul>
  );
}
App
└─ ProductList
   ├─ ProductCard
   ├─ ProductCard
   └─ ProductCard

Zakończenie

W fazie podsumowania należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy rejestrować czasy wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega nieoczekiwanym rachunkom, gdy ścieżka przechodzi z środowiska demonstracyjnego do współdzielonych środowisk. Stan powinien być przechowywany razem z komponentem, który odpowiada za jego modyfikację. Umieszczanie wszystkiego w globalnym magazynie utrudnia wykrycie błędów związanych z czasem wykonywania.

Odnośniki

W fazie referencji należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić dany krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Konfigurację należy przechowywać poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Stan powinien być przechowywany razem z komponentem, który odpowiada za jego zmianę. Umieszczanie wszystkiego w globalnym magazynie utrudnia wykrycie błędów związanych z czasem wykonywania operacji.

Lista kontrolna operacyjna

Podczas pracy nad fazą listy kontrolnej operacyjnej najpierw należy spisać warunki funkcjonowania: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie.

Należy preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy jakiś krok zawiedzie, błąd powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji.

Efekty należy traktować jako synchronizację z otaczającym światem, a nie jako zamiennik wartości wyliczanych podczas renderowania.

Należy ustalić konkretne wersje zależności i zapisać identyfikator obrazu użytego do uruchomienia demonstracji. Reprodukowalność jest ważniejsza od lokalnej wiedzy zespołu.

Tę fazę należy traktować jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Należy nadać nazwy poszczególnym elementom, zdefiniować kryteria sukcesu i odrzucić ciche, częściowe ukończenie zadania.

Efekty należy traktować jako synchronizację z otaczającym światem, a nie jako zamiennik wartości wyliczanych podczas renderowania.

Zanim zaczniesz promować tę architekturę, zamroź wersje, utwórz dokładny zapis dla kluczowych etapów realizacji oraz potwierdź kroki odwracania zmian. Środowiska współdzielone wymagają ograniczeń szybkości, weryfikacji przynależności użytkowników oraz wyraźnego właściciela odpowiedzialnego za rotację haseł. Wolimy nudną niezawodność od pomysłowych, jednorazowych demonstracji.

Uwaga dotycząca d707b555add8: unikaj przechowywania kluczy dostawcy w repozytorium, ustaw ograniczenie liczby tokenów na sesję oraz przechowuj zapisy obok plików testowych, aby późniejsze zmiany modeli pozostawały porównywalne.