Головна / Статті / Практичні зауваження: Компоненти React мають бути чистими — секрет StrictMode

Практичні зауваження: Компоненти React мають бути чистими — секрет StrictMode

Покроковий посібник з практичних нотаток: компоненти React мають бути чистими — секрет StrictMode: контракти, перевірки та слоти для коду для команд, які використовують цю патерн.

1954 слів

Використовуйте цей документ як оновлену версію ідей з статті „React Components Must Be Pure — the Secret Behind StrictMode and the Compiler“ для спеціалістів-операторів: чіткі етапи, впорядковані блоки коду та примітки щодо відновлення, які залишаються після передачі обов’язків. Етап Огляду найкраще функціонує, якщо його розглядати як вимірювану поверхню. Запишіть один ідеальний приклад роботи, один випадок збою та примітки щодо скасування змін перед розширенням обсягу роботи. Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Позначте всі елементи, визначте критерії успіху та не допускайте безповідомного часткового виконання.

Що таке чиста функція?

Для етапу «Що таке чиста функція» необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись визначити прихований стан. Записуйте час виконання та витрати на токени або запити поруч із функціональними результатами. Візуалізація витрат заздалегідь запобігає несподіваним рахункам, коли процес переходить від демо-середовища до спільних середовищ. Розміщуйте стан разом із компонентом, який керує змінами. Перенесення всього до глобального сховища ускладнює виявлення проблем із часом виконання.

// ✅ 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

Компонент також є чистою функцією

Оскільки компонент A є етапом, необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись визначити прихований стан. Конфігурацію слід тримати окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функціоналу мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код. Стан слід розміщувати разом із компонентом, який керує змінами. Розміщення всього в глобальному сховищі ускладнює виявлення проблем з таймінгом.

type Props = { name: string };

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

Порушення правила — не торкатися зовнішніх елементів під час відображення

Для етапу Break the Rule Don необхідно визначити вхідні дані, власника кроку та критерії завершення ще до зміни коду. Оператори мають мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Необхідно документувати як шлях успішного виконання, так і шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Стан слід розміщувати разом із компонентом, який керує змінами. Перенесення всього до глобального сховища ускладнює виявлення проблем із таймінгом. Для етапу Break the Rule Don необхідно визначити вхідні дані, власника кроку та критерії завершення ще до зміни коду. Оператори мають мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Позначте всі елементи, визначте критерії успіху та не допускайте безповідомного часткового завершення.

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>;
}

Виконайте самі — чому з’являється „2“?

Під час роботи над етапом „Виконайте самі — чому“ спочатку запишіть умови виконання: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допоможе зберегти чесність подальших змін у коді. Записуйте час виконання та витрати на токени або запити поруч із результатами функціоналу. Чітке бачення витрат заздалегідь запобігає несподіваним рахункам, коли процес переходить від демо-версії до спільних середовищ. Розглядайте ефекти як синхронізацію з зовнішнім світом, а не як заміну значень, отриманих під час обробки.

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>;
}

Але ви можете змінити „Елементи, створені під час обробки“

Під час роботи над етапом «Але ви можете змінити» спочатку запишіть умови контракту: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Зберігайте конфігурацію окремо від коду додатку. Файли середовища, храни секретних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код. Розглядайте ефекти як синхронізацію з зовнішнім світом, а не як заміну похідних значень під час відображення.

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>
  );
}

Загадка 1 розв’язана — чому StrictMode виконує операції двічі

Під час роботи над етапом «Mystery 1 Solved Why» спочатку запишіть умови контракту: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність у подальших змінах коду. Документуйте як шлях успішного виконання, так і шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої доробки. Розглядайте наслідки як синхронізацію з зовнішнім світом, а не як заміну похідних значень під час обробки даних. Під час роботи над етапом «Mystery 1 Solved Why» спочатку запишіть умови контракту: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність у подальших змінах коду. Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Назвіть створювані елементи, визначте критерії успіху та не допускайте безповідомного часткового завершення роботи.

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

То куди подаються побічні ефекти?

Тож додаткові етапи роботи найкраще використовувати, якщо їх розглядати як вимірювану поверхню. Запишіть один ідеальний результат, один випадок невдачі та примітку про скасування змін перед розширенням обсягу роботи. Записуйте час виконання та витрати на токени чи запити поруч із функціональними результатами. Візуалізація витрат заздалегідь запобігає несподіваним рахункам, коли процес переходить від демо-версії до спільних середовищ. Зберігайте витрати на обробку на низькому рівні та відкладайте дорогі операції на пізніший момент лише після їх вимірювання. Надмірне використання мемоайзації може приховати помилки у застарілих даних.

// 🔴 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>;
}

Загадка 2 розв’язана — чому компілятор React покладається на чистоту коду

„The Mystery 2 Solved: Чому сцена найкраще функціонує, коли її розглядати як вимірювану поверхню“. Зафіксуйте один ідеальний результат, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Тримайте конфігурацію окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код. Зробіть процес генерації зображень дешевим та використовуйте складні алгоритми лише після вимірювань, за допомогою мемоїзації. Надмірна мемоїзація може приховати помилки у застарілих параметрах.

// 🔴 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;

UI як дерево

Інтерфейс Seeing UI як сцена працює найкраще, коли його розглядають як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Документуйте як успішний, так і відновлювальний шляхи роботи разом. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Зберігайте витрати на обробку низькими та відкладайте дорогі операції на пізніший етап лише після вимірювань. Надмірне використання мемоайзерів може приховати баги, пов’язані з застарілими даними. Інтерфейс Seeing UI як сцена працює найкраще, коли його розглядають як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу. Розглядайте цю сцену як контракт між вхідними даними та перевіреними результатами. Позначте всі елементи, визначте критерії успіху та не допускайте мовчазного часткового виконання завдань.

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

Підсумки

На етапі підсумку необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Записуйте час виконання та витрати на токени або запити поруч із функціональними результатами. Відображення витрат заздалегідь запобігає несподіваним рахункам під час переходу з демо-середовища у спільні. Розміщуйте стан разом із компонентом, який керує змінами. Перенесення всього до глобального сховища ускладнює виявлення проблем із часом виконання.

Посилання

На етапі „Посилання“ необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Конфігурацію слід тримати окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функціоналу мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код. Стан слід зберігати разом із компонентом, який керує змінами. Розміщення всього в глобальному сховищі ускладнює виявлення проблем з таймінгом.

Чек-лист для експлуатації

Під час роботи над етапом чек-листу для експлуатації спочатку необхідно описати умови використання: необхідні вхідні дані, сигнал про успіх та наслідки часткової невдачі. Цей чек-лист допомагає зберігати чесність у подальших змінах коду.

Віддавайте перевагу малим, тестованим одиницям перед величезними скриптами. Коли якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану послідовність операцій.

Розглядайте ефекти як синхронізацію з зовнішнім світом, а не як заміну значень, отриманих під час відтворення.

Фіксуйте версії залежностей та записуйте хеш-значення зображення, яке використовувалося під час демонстрації. Відтворюваність краща за „кочові“ знання.

Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Називайте кожен продукт, визначайте критерії успіху та не погоджуйтесь на мовчазне часткове виконання завдань.

Розглядайте ефекти як синхронізацію з зовнішнім світом, а не як заміну значень, отриманих під час відтворення.

Перш ніж запускати стек у продакшн, заморозьте версії, створіть «золотий» запис для критичного шляху та підтвердьте кроки відкату. У спільних середовищах необхідні обмеження на частоту запитів, перевірки прав доступу та чіткий власник для зміни секретів. Віддавайте перевагу надійності перед креативними одноразовими демонстраціями.

Примітка для d707b555add8: не включайте ключі постачальника до репозиторію, встановіть ліміт токенів на сеанс та зберігайте записи поруч із фікстурами для оцінки, щоб подальша заміна моделей залишалася порівнянною.