Кампаненты React павінны быць чыстымі: ідэя за StrictMode і кампілятарам
Чаму React чакае чыстых кампанентаў, як падвойны рэндэр StrictMode выяўляе нячыстасць, дзе месца пабочным эфектам і чаму React Compiler абапіраецца на чысціню.
Крыніцы: React Components Must Be Pure — the Secret Behind StrictMode and the Compiler
Даўжыню часу історыя React складалася з таго, што «напісаў так — і на экране з’явіцца адпаведны элемент». Аднак існуе адна правіла, якое React тылуцяеся, каб кожны компонент яе дазваляў, і яна пояснюе два моменты, якія плутаюць большасць новачакоў: чаму StrictMode вызывае ваш компонент два разы і як Кампайлер React можа так адвацальна оптымізаваць код. Гэта правіла — чыстасць.
Што такое чыстая функцыя
Памятаеце матэматыку: y = f(x) = 2x вяртае 6 для вхідных дадзеных 3. Яна робіць гэта вчора, сёння і пасля ста вызываў, і ніколі не прабуея зменіць што-небудзь паza межамі самой сябе.
Такая функцыя ўважаецца чыстай, што значыць, што выпалоўваюць два умовы.
- Тыя ж вхідныя дадзеныя завжды даюць тыя ж выхідныя рэзультаты.
- У яе няма парадоксальных наследкав, таму ніч гарадзе паza межамі яе сферы вплыву не змінюецца.
Код робіць гэта болей зрозумелым. double ніжэй ёсць чыстым, тады як addToTotal — ні.
// ✅ 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
Компаненты таксама павінны быць чыстымі
React прыяжджае, што кожны компанент — гэта чыстая функцыя. З аднаковымі пропамі і станамі ён павінен ўсега вяртаць аднаковы JSX.
type Props = { name: string };
// ✅ pure — same name, always the same result
function Greeting({ name }: Props) {
return <h1>Hello, {name}!</h1>;
}
Калі даць гэтам компоненту імя „Jane“, то ўсега часу будзе отрымвацца <h1>Hello, Jane!</h1>. React выкарыстоўвае гэтую прыемнасць, каб эфектыва апцэнаваць экран.
Параграф: зміны зовнішняй часты пад час апцэнавання
Проблемы пачынаюцца, калі компонент зміняе ўсё, што знаходзіцца за межамі яго самога, пад час апцэнавання.
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>;
}
Counter падае значэнне зменнай count праз кожныя перрады, таму яго выхад завісіць ад таго, як часта ён вызываецца: 1 — праз першы раз, 2 — праз другі. Обая умовы чыстоты не выпалоўваюць. React можа перрадзіць компонент колькіразоў або ў іншай парады, таму такі компонент стварае багі, якія важка працаваць.
Паспрабуйце і пазнайце, чаму вы атрымліваеце 2
Запусціце той Counter — першым, што пакажаецца на экране, будзе 2, а не 1, і пасля чаго гэта значэнне не зростае. Спачатку гэта выглядае калкольным, але разумеўшы гэта, можна атрымаць цінную науку пра чыстоту коду.
Чаму 2? У рэжыме разработкі StrictMode вызывае функцыю компонента два разы пад час монтавання, таму праз адну процедуру монтавання Counter выпалоўваецца два разы.
- Першы вызыв:
countпераходзіць з 0 на 1, і рэзультатам яе ўжывання ёсць<p>1</p>. - Другій вызов:
countзменшаецца з 1 да 2, і рэзультат —<p>2</p>.
На экране паказываецца другі рэзультат. Калі StrictMode выключаны, функцыя выконваецца аднае раз і вы паказаеце 1.
Чаму ён зупініцца на 2, а не продаваецца да 3, 4 і так далей? Компонент выконваецца толькі тады, калі React яго перэрысоввае. Гэты компонент не мае стата, а яго родны компонент не перэрысовваецца, таму ён выконваецца пад час пачатковага запуску (два разы, заводзячы з StrictMode) і пасля таго застаецца, залішаючы count на тым жа значэнні.
Ёсць кантролюваць змену значэння — трэба выкліканы перэрысовванне. Наступны прыклад робіць гэта праз нажатые на кнопку. (useState будзе рассмотраны дэтальней пазней, таму пакуль проста спрыяйце яго як прыстрой, які перарысоввае экран пад час нажатыя на кнопку.)
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>
);
}
Кожна натыканнея занова атрыбутуе Counter на новы стан празь самае зміну стану, а StrictMode вызывае яго два разы за кожны раз, таму гэтая чысла прырастае на два: 2, 4, 6 і так далей. Без StrictMode яно прырастаў бы на адна за раз.
Адна парада ўжо ўзгляду на гэты дэман. Чаму вообшча стан размешчаецца ўнутрь Counter? Калі вы перамясціце яго да родніка і залішыце Counter як дзеця без пропсаў, натыканнея на кантакт не змяніць показанага 2. Компонент, які не прымае жадных даных, — гэта той, чыя рэзультат ніколі не можа змяніцца, таму React Compiler кэшуе яго і прыпінае перэатрыбутаванне. Чыба побачыць такую зміну, трэба змяніць зовнішнюяя зменную ўнутрь компонента, які насправдзе перэатрыбутуе. Дзівна, але гэтая працэса сама ў сабе ёсць гарным ілюстрацыя таго, што компілятор прыпускае чыстасцю. Тое, як компілятор кэшуе компоненты, заслуговае на аднае самастоятэльнае обсужэнне.
Гэта галоўная мысль. Чысты компанент павінен даўаць той самы рэзультат незалежна ад таго, сколькі разоў яго вызываюць, але гэты компанент залежыць ад колькасці вызываў. StrictMode спецыяльна вызывае яго два разы, ўбачыць гэтую нечыстасць прымаўчыкам.
Рэшэння — не втручацца ў зовнішнія элементы. Пад час атрыбутавання вырахоўваюць рэзультат толькі на адной падставе сэрвысных параметраў і стану самога компанента.
// ✅ 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>
);
}
Важна ўсё тая копія, якая створылася з [...products]. Сортаванне аднойчынага масіву products, які прыходзіць як props, змяніла б гэтыя props і спаксавала бы правілу. Копія была створылася пад час гэтага render, таму ўсё гаразд.
Першая загадка: чаму StrictMode выкананае два разы
У рэжыме розработы StrictMode намеравана вызывае функцыі компонента два разы, каб выявіць компоненты, якія не ўсунутыя. Гэта той самы обгорак <StrictMode>, які быў размешчаны наваколе аплікацыі пад час стварэння праекта.
// main.tsx — in development, the components inside this render twice
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
Чысты компанент дае ідэнтычныя рэзультаты праз два запуску, таму нічога не выходзіць. Компанент, такі як Counter вышэй, які выкарыстоўвае зовнішнюяя зменную, адразу паказвае сігналы апазоры: рэзультаты зменяюцца або значэнне удвойваецца. StrictMode выявляе гэтыя сімптамы ў процэсе разработкі, задоўга перш чым яны ператворыцца на проблемы, якія будзе важка выявіць, калі дапрацоўанне будзе запусцана.
Такім чынам, двойны запуск — гэта не баг. Гэта перакананне ў стане компанента. Калі два запуску не вызываюць проблем, значыць компанент чысты. У версіі для рэальнага выкарыстоўвання ён запускаецца толькі аднае раз.
Дзе паследнія эфекты дзеяўцяў маюць быць
Вы можаце запытацца: логаванне, адправка дадзенаў на сервер і запуск таймера явна неабходныя, то дзе яны маюць быць? Будзь-дзе, кроме самага працэса атрыбутавання.
- Рэакцыя на дзеяўцы корыстніка, такія як клікі і набіранне тэксту, мае адбывацца ў апрацоўшчыках запуску змян.
- Сінхронізацыя з зовнішнім сістэмам пасля адражэння экрана патрапляе пад раздзел «Эфекты».
Спадзейваецца, калі вы фіксуеце перагляд сторанкі прымусова ў тэле рэндэра: гэта будзе выконвана ў кожны раз, калі вядзецца рэндэр, і ўдвойчы часта пад рэжымам 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>;
}
Зберагачыце тэла рэндараўчыка чыстымі і перакладзіце пабочныя эфекты на ўпарэджаныя для іх месца. Гэты прынцып стаўчыця знова і знова.
Другая загадка: чаму кампайляр завісіць ад чыстоты
Калі вы увімкнулі React Compiler, можаце памятаць, што ён заменяе ручна напісаныя useMemo і useCallback. Чыстота — гэта тое, што дазволяе гэта здарыцца.
Кампайляр працуе на падазрэнні, што компаненты ўсе чыстыя. Паколькі адно і тое ж уведзенне гарантавана дае адно і тое ж выведзенне, ён вважае безпечным павторна выкарыстоўваць пярэйшы рэзультат, калі уведзенні не зменіліся, і автаматычна дагаўляе мемаізацыю. Калі компанент зменіў ўмовы пад час рэндараўчыка, павторнае выкарыстоўванне можа даць некарэктныя рэзультаты, таму кампайляр прыпіняе оптымізацыю для такіх компанентаў, якія наражаюцься на гэтую праблему. Правіло ESLint, увімкнута разам з кампайляром, паведамляе вас, які саме компанент ёсць прычыной.
// 🔴 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;
Компанент, напісаны чыстаю спосабам, паказуецца ў DevTools з атрыбутам "Memo", нават якшо не было выкарыстоўвана дырэктыва useMemo.
Корача кажучы, чыстасць — гэта праблэма-прыяўнік для таго, каб кампайляр мог усёлякія дапамогі. Пішыце чыстыя компаненты, і павышэнне працэсавой эфектыўнасці будзе настаўляцца апошнім чынам. Џедынае, пра што трэба турбавацца, — гэта пісьмо чыстага коду.
Відбіранне UI як дрэва
Ішчо адна перспектыва дапамагае пачувстваваць сітуацыю цэлысна. Компаненты знаходзяцца ўнутры іншых компанентаў: App знаходзіцца ўнутры ProductList, які знаходзіцца ўнутры калькі ProductCard-аў. Гэта внутрошняе структуруванне сама па сабе стварае дрэва.
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
Чым даўжэй вы працуеце, тым больш значэнне набірае дрэваўскі відобразэнне. Даныя у форме праіслаўных параметраў тэчу па ўсім дрэву з верху да ніжней часткі, а калі будзеце вучыцца пра стан, выбір месца ў дрэве, дзе яго размясціць, стане ключовым элементам вашага дызайну. Згадвайце прыклад «экран = дрэва компонентаў», і нават складныя інтэрфейсы можна структурна аналізаваць.
Падсумак
- React спрацоўвае кожны компонент як чыстую функцыю: тыя ж праіслаўныя параметры і стан, той жа JSX.
- Пад час відобразэння не трэба меняць нічога зовнішняга, укладаючы ў тое жа зменныя, праіслаўныя параметры і стан. Значэнні, створаныя нова пад час тэкушчага відобразэння, можна зменяць, напрыклад, пад час сортавання.
- Двойная рэндарызацыя ў рэжыме StrictMode пад час розработы ўжо ў сабе ўключае перагляд чыстоты компонентаў; нечыстыя компоненты будуць выяўлены, а чыстыя працююць без змян.
- Пабочныя эфекты трэба размешчаць у апрацоўшчыках запуску адбыўчых здарэнняў або ў функціях Effects, а не ў тэле функціі відобразэння.
- Чыстасць — гэта тое, што дазволяе кампайляру React аптэмізаваць код аўтаматычна, таму напісанне чыстага коду прыносіць выгоду з точкі зору працэздатнасці.
- Інтэрфейс — гэта дрэва компанентаў, і даны працягваюцца па ўсім яго шляху.
Дапытліва, экран зараз як застойваны зябр: вы можэце склadaць яго з компанентаў, пісаць маркап з JSX, перадаваць даны праз пропы і ствараць дынамічныя відображэнні з выкарыстоўванням умов і ціклаў, але нажатыя кнопкі нічога не робяць. Наступны крок — заставіць экран рэагаваць на корыстніка, і ён пачынаецца з апрацоўкі запускаў.
Афіцыйныя сторанкі дакументацыі React пра падтрымкі чыстасці компанентаў і пра розумеўце інтэрфейса як дрэва падробна раскрываюць тэтыя тэмы.