Захоўванне кампанентаў чыстымі

Некаторыя JavaScript функцыі з’яўляюцца чыстымі. Чыстыя функцыі выконваюць толькі вылічэнне і больш нічога. Калі вы пільна сочыце за тым, каб вашыя кампаненты заставаліся чыстымі функцыямі, тады вы зможаце пазбегнуць цэлага класа незразумелых памылак і непрадказальных паводзін па меры росту вашай кодавай базы. Аднак, каб атрымаць гэтыя перавагі, трэба прытрымлівацца некалькіх правіл.

You will learn

  • Што такое чысціня і як яна дапамагае пазбегнуць памылак
  • Як захоўваць кампаненты чыстымі, не дапускаючы змяненняў на этапе рэндэрынгу
  • Як выкарыстоўваць строгі рэжым (Strict Mode) для пошуку памылак у кампанентах

Чысціня: кампаненты як формулы

У інфарматыцы (і асабліва ў свеце функцыянальнага праграмавання) чыстыя функцыі — гэта функцыі з наступнымі характарыстыкамі:

  • Займаюцца сваёй справай. Яны не змяняюць аб’екты або пераменныя, якія існавалі да выкліку функцыі.
  • Вяртаюць прадказальны вынік. Пры аднолькавых уваходных даных чыстая функцыя заўсёды вяртае аднолькавы вынік.

Магчыма, вы ўжо знаёмыя з адным прыкладам чыстых функцый: матэматычнымі формуламі.

Разгледзім наступную матэматычную формулу: y = 2x.

Калі x = 2, тады y = 4. Заўсёды.

Калі x = 3, тады y = 6. Заўсёды.

Калі x = 3, тады y не будзе часам роўным 9 або –1, або 2,5 у залежнасці ад часу сутак або стану фондавага рынку.

Калі y = 2x і x = 3, тады y заўсёды будзе 6.

Калі б мы запісалі гэта як JavaScript функцыю, яна б выглядала наступным чынам:

function double(number) {
return 2 * number;
}

У прыкладзе вышэй doubleчыстая функцыя. Калі вы перадасце ёй 3, яна верне 6. Заўсёды.

React пабудаваны вакол гэтай канцэпцыі. React мяркуе, што кожны напісаны вамі кампанент з’яўляецца чыстай функцыяй. Гэта азначае, што вашыя кампаненты React павінны для аднолькавага ўводу заўсёды вяртаць адзін і той жа JSX:

function Recipe({ drinkers }) {
  return (
    <ol>    
      <li>Закіпяціце {drinkers} шклянкі(-ак) вады.</li>
      <li>Дадайце {drinkers} лыжку(-ак) чаю і {0,5 * drinkers} лыжку(-ак) спецый.</li>
      <li>Дадайце {0,5 * drinkers} шклянкі(-ак) кіпячонага малака і цукар па гусце.</li>
    </ol>
  );
}

export default function App() {
  return (
    <section>
      <h1>Рэцэпт чаю са спецыямі</h1>
      <h2>Для дваіх</h2>
      <Recipe drinkers={2} />
      <h2>Для кампаніі</h2>
      <Recipe drinkers={4} />
    </section>
  );
}

Калі вы перадаяце drinkers={2} у кампанент Recipe, ён верне JSX, які змяшчае 2 шклянкі(-ак) вады. Заўсёды.

Калі вы перадаяце drinkers={4} — ён верне JSX, які змяшчае 4 шклянкі(-ак) вады. Заўсёды.

Гэтак жа, як матэматычная формула.

Вы можаце разглядаць свой кампанент як рэцэпт: калі яго прытрымлівацца і не ўводзіць новыя інгрэдыенты ў працэсе гатавання, то кожны раз атрымаецца адна і тая ж страва. «Страва» — гэта JSX, які кампанент вяртае React для рэндэру.

Рэцэпт чаю для х чалавек: вазьміце х шклянак вады, дадайце х лыжак гарбаты і 0,5х лыжкі спецый і 0,5х шклянкі малака

Illustrated by Rachel Lee Nabors

Пабочныя эфекты: (не)прадбачаныя наступствы

Працэс рэндэрынгу ў React заўсёды павінен быць чыстым. Кампаненты павінны толькі вяртаць свой JSX, а не змяняць якія-небудзь аб’екты або пераменныя, якія існавалі да рэндэрынгу — гэта зробіць кампаненты нячыстымі!

Вось кампанент, які парушае гэтае правіла:

let guest = 0;

function Cup() {
  // Дрэнна: змяненне існуючай пераменнай!
  guest = guest + 1;
  return <h2>Кубак чаю для госця #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup />
      <Cup />
      <Cup />
    </>
  );
}

Гэты кампанент чытае і запісвае пераменную guest, абвешчаную па-за яго межамі. Гэта азначае, што выклікаўшы гэты кампанент некалькі разоў — вы кожны раз атрымаеце розны JSX! І больш за тое, калі іншыя кампаненты чытаюць пераменную guest, яны таксама будуць ствараць розны JSX, у залежнасці ад таго, калі адбываўся рэндэрынг! Гэта непрадказальна.

Вяртаючыся да нашай формулы y = 2x, цяпер нават калі x = 2, мы больш не можам верыць таму, што y = 4. Нашы тэсты могуць перастаць працаваць, нашы карыстальнікі могуць быць збянтэжаны, самалёты могуць пачаць падаць з неба — вы бачыце, як гэта можа прывесці да нечаканых памылак!

Вы можаце выправіць гэты кампанент, перадаўшы guest у якасці пропса:

function Cup({ guest }) {
  return <h2>Кубак чаю для госця #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup guest={1} />
      <Cup guest={2} />
      <Cup guest={3} />
    </>
  );
}

Цяпер кампанент стаў чыстым, бо JSX, які ён вяртае, залежыць толькі ад пропса guest.

Наогул, вы не павінны разлічваць на тое, што вашы кампаненты будуць рэндэрыцца ў пэўным парадку. Не мае значэння, калі вы выклікаеце y = 2x да або пасля y = 5x: абедзве формулы будуць разлічвацца незалежна адна ад адной. Такім да чынам, кожны кампанент павінен «думаць сам за сябе» і не спрабаваць каардынаваць дзеянні з іншымі кампанентамі або залежаць ад іх падчас рэндэрынгу. Рэндэрынг падобны на школьны экзамен: кожны кампанент павінен вылічваць JSX самастойна!

Deep Dive

Выяўленне нячыстых вылічэнняў з дапамогай StrictMode

Хоць вы, магчыма, яшчэ не выкарыстоўвалі іх усе, але ў React ёсць тры віды ўваходных даных, якія вы можаце чытаць падчас рэндэрынгу: пропсы, стан і кантэкст. Вы заўсёды павінны разглядаць іх як даныя толькі для чытання.

Калі вы хочаце нешта змяніць у адказ на ўвод карыстальніка, вам трэба задаць стан замест змены пераменнай. Вы ніколі не павінны змяняць раней існуючыя пераменныя або аб’екты падчас рэндэрынгу кампанента.

React прапануе «строгі рэжым» (Strict Mode), у якім ён двойчы выклікае функцыю кожнага кампанента падчас распрацоўкі. Выклікаючы функцыі кампанента двойчы, строгі рэжым дапамагае знайсці кампаненты, якія парушаюць правілы чысціні.

Звярніце ўвагу, як у арыгінальным прыкладзе адлюстроўваліся надпісы «для госця #2», «для госця #4» і «для госця #6» замест «для госця #1», «для госця #2» і «для госця #3». Зыходная функцыя не была чыстай, таму яе падвойны выклік не даў чаканы вынік. Але выпраўленая чыстая функцыя заўсёды працуе, нават калі яна выклікаецца двойчы. Чыстыя функцыі толькі робяць вылічэнні, таму двайны іх выклік нічога не зменіць—гэтак жа, як двайны выклік функцыі double(2) не змяняе тое, што вяртаецца, і рашэнне ўраўнення y = 2x зробленае двойчы не змяняе значэнне y. Пры аднолькавых уваходных даных — аднолькавы вынік. Заўсёды.

Строгі рэжым не ўплывае на працоўнае асяроддзе, таму ён не запаволіць працу праграмы для вашых карыстальнікаў. Каб уключыць строгі рэжым, вы можаце абгарнуць каранёвы кампанент у <React.StrictMode>. Некаторыя фрэймворкі робяць гэта прадвызначана.

Лакальная мутацыя: маленькі сакрэт вашага кампанента

У прыведзеным вышэй прыкладзе праблема была ў тым, што кампанент змяніў існуючую пераменную падчас рэндэрынгу. Гэта часта называюць «мутацыяй», каб гучала крыху страшней. Чыстыя функцыі не муціруюць пераменныя па-за вобласцю бачнасці функцыі або аб’екты, якія былі створаны перад выклікам — гэта робіць іх нячыстымі!

Аднак цалкам нармальна змяняць пераменныя і аб’екты, якія вы толькі што стварылі падчас рэндэрынгу. У гэтым прыкладзе мы ствараем масіў [], прысвойваем яго пераменнай cups, а затым дадаем (з дапамогай метаду push) у яго тузін «кубкаў»:

function Cup({ guest }) {
  return <h2>Кубак чаю для госця #{guest}</h2>;
}

export default function TeaGathering() {
  let cups = [];
  for (let i = 1; i <= 12; i++) {
    cups.push(<Cup key={i} guest={i} />);
  }
  return cups;
}

Калі б пераменная cups або масіў [] былі створаны па-за функцыяй TeaGathering, то гэта была б вялікая праблема! Вы б змянялі існуючы аб’ект, дадаючы элементы ў гэты масіў.

Аднак, усё ў парадку, бо вы стварылі іх падчас таго ж рэндэру, унутры кампанента TeaGathering. Ніхто па-за межамі TeaGathering ніколі не даведаецца, што адбылося. Гэта называецца «лакальная мутацыя» — гэта як маленькі сакрэт вашага кампанента.

Дзе вы можаце выклікаць пабочныя эфекты

У той час як функцыянальнае праграмаванне ў значнай ступені абапіраецца на чысціню, у нейкі момант дзесьці штосьці павінна змяніцца. У гэтым і ёсць сэнс праграмавання! Гэтыя змены — абнаўленне экрана, запуск анімацыі, змяненне даных — называюцца пабочнымі эфектамі. Гэта з’явы, якія адбываюцца «збоку», а не падчас рэндэру.

У React пабочныя эфекты звычайна знаходзяцца ўнутры апрацоўшчыкаў падзей. Апрацоўшчыкі падзей — гэта функцыі, якія React запускае, калі вы выконваеце некаторыя дзеянні, напрыклад, націскаеце кнопку. Нягледзячы на тое, што апрацоўшчыкі падзей аб’яўляюцца ўнутры вашага кампанента, яны не запускаюцца падчас рэндэрынгу! Такім чынам, апрацоўшчыкі падзей не павінны быць чыстымі.

Калі вы перабралі ўсе іншыя варыянты і ўсё роўна не можаце знайсці правільны апрацоўшчык падзей для вашага пабочнага эфекту, вы можаце далучыць яго да вернутага JSX з дапамогай выкліку useEffect у вашым кампаненце. З дапамогай useEffect мы кажам React выканаць пабочны эфект пазней, пасля рэндэрынгу, калі пабочныя эфекты дазволены. Аднак гэты падыход павінен быць вашым апошнім сродкам.

Калі гэта магчыма, спрабуйце апісаць сваю логіку толькі з дапамогай рэндэрынгу. Вы будзеце здзіўлены, наколькі далёка гэта можа вас завесці!

Deep Dive

Чаму React клапоціцца пра чысціню?

Напісанне чыстых функцый патрабуе пэўнай звычкі і дысцыпліны. Але таксама адкрывае надзвычайныя магчымасці:

  • Вашы кампаненты могуць працаваць у іншым асяроддзі — напрыклад, на серверы! Паколькі яны вяртаюць аднолькавы вынік для аднолькавых уваходных даных, адзін кампанент можа абслугоўваць мноства запытаў карыстальнікаў.
  • Вы можаце палепшыць прадукцыйнасць, прапусціўшы рэндэрынг кампанентаў, уваходныя даныя якіх не змяніліся. Чыстыя функцыі бяспечна кэшаваць, таму што яны заўсёды вяртаюць аднолькавыя вынікі.
  • Калі некаторыя даныя змяняюцца падчас рэндэрынгу глыбокага дрэва кампанентаў, React можа перазапусціць рэндэрынг, не марнуючы час на завяршэнне састарэлага рэндэрынгу. Чысціня дазваляе бяспечна спыніць вылічэнне ў любы момант.

Кожная новая функцыя React, якую мы ствараем, выкарыстоўвае перавагі чысціні. Ад выбаркі даных да анімацыі і прадукцыйнасці, захаванне кампанентаў чыстымі раскрывае моц парадыгмы React.

Recap

  • Кампаненты павінны быць чыстымі, што азначае:
    • Займаюцца сваёй справай. Яны не змяняюць аб’екты або пераменныя, якія існавалі да рэндэрынгу.
    • Вяртаюць прадказальны вынік. Пры аднолькавых уваходных даных кампанент заўсёды вяртае аднолькавы JSX.
  • Рэндэрынг можа адбыцца ў любы момант, таму кампаненты не павінны залежаць ад іх паслядоўнасці рэндэрынгу.
  • Вы не павінны змяняць уваходныя даныя, якія вашы кампаненты выкарыстоўваюць для рэндэрынгу. Гэта могуць быць пропсы, стан або кантэкст. Каб абнавіць экран, «задайце» (set) стан замест таго, каб муціраваць раней існуючыя аб’екты.
  • Імкніцеся апісваць логіку вашага кампанента ў JSX, які вы вяртаеце. Калі вам трэба нешта змяніць, лепш гэта зрабіць у апрацоўшчыку падзей. У крайнім выпадку, карыстайцеся useEffect.
  • Напісанне чыстых функцый патрабуе практыкі, але гэта раскрывае моц парадыгмы React.

Challenge 1 of 3:
Выправіць зламаны гадзіннік

Гэты кампанент спрабуе задаць для <h1> CSS клас "night" у перыяд з поўначы да шасці гадзін раніцы і "day" ва ўвесь астатні час. Аднак гэта не працуе. Ці можаце вы выправіць гэты кампанент?

Вы можаце праверыць, ці працуе ваша рашэнне, часова змяніўшы часавы пояс на вашым камп’ютары. Калі бягучы час паміж поўначчу і шасцю раніцы, гадзіннік павінен мець інвертаваныя колеры!

export default function Clock({ time }) {
  let hours = time.getHours();
  if (hours >= 0 && hours <= 6) {
    document.getElementById('time').className = 'night';
  } else {
    document.getElementById('time').className = 'day';
  }
  return (
    <h1 id="time">
      {time.toLocaleTimeString()}
    </h1>
  );
}