Головна / Статті / Створення ментальної моделі для React: узгодження, стан та хуки

Створення ментальної моделі для React: узгодження, стан та хуки

Дізнайтеся про логіку, яка стоїть за основними концепціями React — узгодження даних, компоненти, пропси, стан та хуки — щоб розвинути інтуїцію замість того, щоб запам’ятовувати API.

2998 слів

Якщо ви вже вмієте писати код, але ще не глибоко знайомі з React — або ж торкнулися його лише поверхнево, — ця стаття написана саме для вас.

Коли ви починаєте вивчати React, буває спокуса одразу перейти до useState, useEffect, props, hooks та довгого списку інших API. Ви можете опанувати синтаксис, створити щось, що працює, але все одно не зрозуміти чому React поводиться саме так.

Мета цієї статті — усунути цю прогалину.

Замість того, щоб розглядати React як набір API для запам’ятовування, ми розглянемо причини його проектування та те, як всі елементи взаємопов’язані. Синтаксис має значення, але його набагато легше засвоїти, коли ви розумієте, що відбувається під ним. Тож замість того, щоб починати з як писати код на React, спочатку ми розберемо як мислити щодо React.

Ми розглянемо компоненти, пропси, стан, відображення, узгодження, хуки, проникнення через пропси, контекст та маршрутизацію, пояснюючи, як кожна з цих концепцій допомагає вирішувати конкретну проблему. Мета не в тому, щоб перелічити всі API, які пропонує React, а в тому, щоб надати вам ментальну модель, яка допоможе зрозуміти ці API після їхнього використання.

Ви, ймовірно, чули, що однією з причин такої швидкості React є певний розумний алгоритм. Це може звучати майже як магія — як бібліотека на JavaScript може так ефективно оновлювати інтерфейс?

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

Узгодження — алгоритм, який зробив React можливим

Порівняння двох дерев DOM з нуля та обчислення мінімального набору змін, необхідних для оновлення, — це задача, вирішення якої може зайняти приблизно O(n³) часу.

Але що, якщо процес узгодження ґрунтується на кількох розумних припущеннях, і ми надамо йому кілька підказок щодо того, де, ймовірно, будуть зміни?

Ось так! Складність знижується приблизно до O(n).

Це означає, що підхід, який наївно зайняв би близько 31 року для виконання, скорочується до приблизно 16 хвилин — лише завдяки використанню припущень та невеликої допомоги від розробника.

Зачекайте… підказки? Що саме це за підказки та як ми їх надаємо?

Немає потреби хвилюватися. Це простіше, ніж здається, і ми скоро це пояснимо.

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

Але навіщо взагалі використовувати React?

Чому React?

Незалежно від того, що вам кажуть інші, при переході від звичайних файлів HTML, CSS та JavaScript до концепцій, таких як компоненти, хуки, пропси та стан, виникає справжня психологічна адаптація.

Порівняно з такими фреймворками, як FastAPI чи Go, React справді може здаватися складнішим для опанування.

Але справа у тому, що ця складність зосереджена на початковому етапі.

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

Вам не потрібно одночасно запам’ятовувати всю свою програму.

Натомість ви можете зосередитися на одному компоненті — на тих даних, які він отримує, що йому потрібно відстежувати, та на тому, як має оновлюватися його відображений результат.

Саме цей розділений на частини спосіб мислення робить створення та підтримку складних інтерфейсів керованими. І в кінцевому підсумку саме це є тим, що насправді має значення для розробників.

Створення коду, який легше створювати, розуміти, змінювати та підтримувати. Мета тут — допомогти вам досягти цього.

Чотири основи React :—

Компоненти

Компонент по суті — це функція JavaScript, яка приймає один об’єкт та повертає UI, написаний у форматі JSX.

JSX — це розширення синтаксису JavaScript, яке дозволяє вбудовувати маркування, схоже на HTML, прямо у код JavaScript. Ви можете стилізувати елементи за допомогою звичайного CSS або бібліотеки типу Tailwind CSS.

Будь-який достатньо складний додаток на React, у кінцевому підсумку, є просто мережею компонентів, які передають дані один одному.

Припустимо, ви ніколи раніше не працювали з React — ви все одно можете зрозуміти, що робить компонент.

const data = {
  question: "What is React?",
  answer: "A JavaScript library for building user interfaces"
};
function FlashCard(data) {
  return (
   <div className="border rounded-lg bg-gray-50 p-4">
       <h2>{data.question}</h2>
       <p>{data.answer}</p>
    </div>
   );
}

Якщо ігнорувати кілька особливостей синтаксису React, це по суті і є сутью цього фреймворку.

Це не так вже й погано, чи не так?

По суті ми передаємо певні дані у функцію та отримуємо елемент інтерфейсу користувача.

Тож як розробник, ваше справжнє завдання — створювати чисті компоненти, постійно пам’ятаючи про три запитання:

  1. Яку інформацію він отримує?
  2. Яку інформацію він зберігає?
  3. Як має змінюватися його вигляд?

Що може бути складним у кількох параметрах та декількох змінних? Чи не можемо ми просто передати потрібні дані та зберегти все, що хочемо, у локальних змінних?

Частково — але не зовсім.

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

Що відбувається з іншим продавцем? Він продовжує наводити стару ціну, не підозрюючи нічого.

Тепер уявіть, що керівник публікує це оновлення у груповому чаті, до якого входять усі. Як тільки ціна змінюється, вся команда бачить це миттєво.

Це, по суті, проблема, яку було створено React для вирішення.

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

Props

Чи можна замінити function User(name, age, city) та function User(name, city) один на одного? А як щодо function User(city, name)?

Типова функція, яка спирається на позиційні параметри, може приймати лише певну кількість аргументів у конкретному порядку — і пам’ятайте, що компонент — це просто функція.

Тепер уявіть, що ви створили компонент, який відображає ім’я та вік користувача, і зараз його використовують у 67 різних місцях у вашому кодбазі. Потім ваш керівник просить вас також показати місто користувача. Оновлення кожного випадку використання займе години роботи.

Але що, якби функція була розроблена так, щоб старі виклики продовжували працювати без змін, тоді як нові виклики могли за бажанням передавати додаткові дані?

Інструмент, який вам потрібен у цьому випадку, — це єдиний об’єкт, який замінює список параметрів. React називає це props.

Props — це просто спосіб, яким React об’єднує всі дані, передані компоненту, у один об’єкт.

/** So a component like this one **/
function User({ name = "Guest", age = 18, city = "Unknown" }) {
    return (
        <div>
            <h2>{name}</h2>
            <p>{age} years old</p>
            <p>{city}</p>
        </div>
    );
}
/** Can be used in ways like **/
<User />                                    /** Guest, 18, Unknown **/
<User name="Pritam" />                      /** Pritam, 18, Unknown **/
<User name="Rahul" age={21} />              /** Rahul, 21, Unknown **/
<User name="Priya" age={22} city="Delhi" /> /** Priya, 22, Delhi **/

Стани

Чи повинен був менеджер дилерського центру зберігати нову ціну лише для себе? Сказати лише одному продавцю? Обом? Чи оголосити про неї всім у дилерському центрі, включаючи охоронців та працівників з прибирання?

Уявіть собі коробку для зберігання, яку ви заповнюєте своїми речами. Чи можете ви, лише поглянувши на неї ззовні, визначити, чи щось усередині змінилося? А якщо коробку перемістити на іншу полицю? Принаймні ви помітите, що її положення змінилося, і зробите висновок, що щось сталося.

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

Це означає, що має існувати спосіб передавати оновлені значення тим, хто від них залежить, а також спосіб, щоб ці залежні елементи дізнавалися, коли їхнє значення змінилося.

const [count, setCount] = useState(initialCount);

Чи стикалися ви раніше з цим викликом useState?

Він передає компоненту дві речі:

  • count — поточне значення state.
  • setCount — функція, яку ви викликаєте, щоб попросити оновити це значення state.

То у чому проблема, якщо просто написати value = 5?

У React немає способу дізнатися, що щось усередині цього елемента змінилося!

Іншими словами, вам потрібен механізм, щоб повідомити React: "Гей, я оновив це значення, можливо, вам варто оновити інтерфейс."

Чи це та сама натяк, про яку йшлося раніше? Частково — так, частково — ні.

useState дає можливість зберігати значення, запитувати про його оновлення та водночас повідомляти React про відбулася зміну. Але чому React потрібно прямо про це повідомляти?

Тому що інакше ви будете поводитися як той неуважний менеджер автосалону.

Пам’ятаєте ту помилку? Менеджер змінив ціну, оновив свою копію табло, але забув повідомити інших. Ви розумніші — ви просто викличете setCount().

Що ж насправді відбувається після того, як ви викликаєте setCount()?

React бере новий стан, переробляє компонент, щоб визначити, яким має бути інтерфейс користувача, а потім використовує процес узгодження, щоб з’ясувати, що саме потрібно змінити на екрані.

Ви просто повідомили React, що щось змінилося. Узгодження визначає що змінилося та що потребує оновлення в результаті.

Hooks

useState() — це вбудована функція, яка дозволяє компоненту зберігати частину стану та надає спосіб звернутися за її оновленням.

Коли цей стан оновлюється, можуть відбутися дві речі:

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

    useRef() — корисний, коли вам потрібен контейнер для значення, яке зовсім не пов’язане з інтерфейсом.

    const count = useRef(0);
    count.current++;
    

    Отже, загальне правило таке: якщо зміна значення має також змінити інтерфейс, використовуйте useState(). Якщо потрібно змінити лише саме значення без жодних наслідків для відображення, використовуйте useRef().

    У нього є ще одне поширене застосування — посилання на справжні елементи DOM, але поки цієї концепції достатньо.

    useEffect() — використовується тоді, коли потрібно, щоб якась операція виконувалася після того, як React завершить відображення інтерфейсу.

    useEffect(() => {
        console.log("Runs after every render");
    });
    
    useEffect(() => {
        console.log("Runs once after the initial render");
    }, []);
    
    useEffect(() => {
        console.log("After the initial render and whenever count changes");
    }, [count]); /** Dependencies go here **/
    

    useContext() — уявіть собі дерево компонентів, де певні дані, наприклад name, мають пройти крізь кілька рівнів компонентів, щоб дістатися глибоко вкладеному компоненту UserName.

    Тепер уявіть, що це дерево розширюється, і кілька наборів даних проходять крізь компоненти, які навіть не використовують їх безпосередньо.

    Такий підхід називається prop drilling.

    API Context від React пропонує спосіб обійти цю проблему: він дозволяє зробити дані доступними у будь-якій глибині дерева, не передаючи їх вручну крізь кожен компонент посередині.

    const ParentContext = createContext(null)
    
    <ParentContext value={money}>
      <ChildComponent />
    </ ParentContext>
    
    function ChildComponent() {
      const money = useContext(ParentContext);
    
      return <p>Money: {money}</p>;
    }
    

    Існує ще багато інших хуків, і варто спробувати їх використовувати самостійно.

    До цього моменту ми говорили про те, як React організовує компоненти та як дані передаються між ними. Але справжнє додатку також має вирішити, які частини цього інтерфейсу мають відображатися за певними URL-адресами.

    React Router

    React дозволяє створювати інтерфейси з компонентів, які оновлюються самостійно, без необхідності перезавантажувати всю сторінку браузером. Але справжнє додатку зазвичай потребує більше ніж однієї сторінки, що ставить нове питання.

    Що відбувається, коли вашому додатку потрібно кілька різних відображень?

    Можливо, ви захочете щось на кшталт:

    /home → Головна сторінка /dashboard → Панель керування /profile → Профіль

    Якщо ви під’єднаєте це за допомогою звичайних тегів HTML, натискання на них викликає повну навігацію браузером. Вся сторінка видаляється, і додаток знову запускається з нуля за новою адресою.

    Насправді ви хочете, щоб URL оновлювався, поки React тихо визначає які компоненти потрібно замінити, не видаляючи все інше.

    Саме цю проблему вирішує React Router.

    Уявіть собі це як шар, який відповідає за зв’язок URL із компонентами.

    Наприклад:

    <BrowserRouter>
      <Routes>
        <Route path="/home" element={<Home />} />
        <Route path="/dashboard" element={<Dashboard />} />
      </Routes>
    </BrowserRouter>
    

    React Router аналізує поточний URL та відображає компонент, який до нього прив’язаний. BrowserRouter використовує API історії браузера для обробки навігації на клієнтській стороні, а Routes вибирає той Route, який найкраще відповідає поточному шляху.

    Проте існує ще одна складність, з якою потрібно рахуватися.

    Що робити, якщо ви не хочете, щоб весь екран перероблявся?

    ┌─────────────────────────────┐
    │          Header             │
    ├──────────┬──────────────────┤
    │          │                  │
    │  Menu    │   Page content   │
    │          │                  │
    ├──────────┴──────────────────┤
    │          Footer             │
    └─────────────────────────────┘
    

    Переход від /home до /dashboard не повинен призводити до зникнення та знову появи заголовка, бічної панелі чи футера. Має змінюватися лише основна область вмісту.

    Саме для такої ситуації створені вкладені маршрути та елемент <Outlet />.

    function Layout() {
      return (
        <>
          <Header />
          <Menu />
          <Outlet />
          <Footer />
        </>
      );
    }
    

    Самі маршрути можна вкладати один у інший:

    <Routes>
      <Route element={<Layout />}>
        <Route index element={<Home />} />
        <Route path="dashboard" element={<Dashboard />} />
      </Route>
    </Routes>
    

    За такої налаштовки елемент Layout залишається активним, а React Router підставляє відповідний дочірній маршрут всередину <Outlet />. Згідно з власною документацією React Router, елемент <Outlet /> позначає місце, де відображатиметься відповідний дочірній маршрут.

    Шлях index відповідає <Home /> шляху /, тож це є стандартною візуалізацією, яка відображається у компоненті outlet, коли немає більш конкретного шляху.

    Отже, перехід від /home до /dashboard насправді не означає заміни всієї сторінки. Це більше схоже на наступне:

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

    Що далі?

    Як тільки ви чітко зрозумієте проблеми, які вирішує React, та механізми, які він використовує для їх вирішення, варто приділити час ознайомленню з реальними кодовими базами, щоб побачити, як досвідчені команди структурують свої додатки.

    Шукайте репозиторій, який демонструє, як можна організувати та спроєктувати продакшн-додаток на React у великих масштабах.

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

    Також варто знайти приклад, який показує, як React можна поєднати з Redux у функціональному додатку.

    Якщо проект, який ви знайшли, є старішим, не вважайте його підходи актуальним стандартом для React. Використовуйте його натомість для вивчення того, як великий додаток можна розділити на частини та як Redux вписується в цю структуру.

    Як тільки ви звикнете до типової структури файлів у React та зможете самостійно створювати прості компоненти, наступним кроком буде навчання створенню додатків, які працюють у масштабі.

    Масштабування React-додатку створює власні проблеми, серед яких:

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

    Більш практичний підхід — почати розробляти, зіткнутися з конкретною проблемою, а потім дізнатися ту концепцію чи інструмент, які допоможуть її вирішити.

    Зрештою, метою вивчення React ніколи не було запам’ятовування його API. Метою було зрозуміти чому існують ці API та як мислити щодо проблем, для вирішення яких вони були створені.

    Пов’язана література

  • Розуміння JavaScript Proxy: пастки, Reflect та реактивні шаблони — Дізнайтеся, як об’єкти Proxy та Reflect у JavaScript перехоплюють доступ до властивостей для забезпечення перевірки даних, віртуальних властивостей та роботи з реактивними фреймворками.
  • Керування станами інтерфейсу в реальному світі за допомогою умовного відображення в React — Дізнайтеся, як створювати інтерфейси для автентифікації, ролей, дозволів, стану завантаження, помилок та порожнього стану в React за допомогою практичних шаблонів умовного відображення.
  • React Components 101: Створення повторно використовуваних та легко підтримуваних елементів інтерфейсу — Дізнайтеся, чому розділення інтерфейсу на невеликі компоненти React покращує їх повторне використання, читабельність та співпрацю в команді, а потім створіть свій перший функціональний компонент.
  • Набір повторно використовуваних користувацьких гаків для кожного нового проекту на React — Ознайомтесь із підібраним набором користувацьких гаків для React, які включають функції зберігання даних, затримки виконання операцій, обробки кліків та отримання даних, і які допомагають усунути повторюваний базовий код у нових проектах.
  • Припиніть синхронізацію стану з useEffect: безпечніший патерн у React — Дізнайтеся, чому використання useEffect для синхронізації похідного стану спричиняє ситуації змагання та зайве відображення елементів, та як замінити його на обчислення під час відображення та використання атрибута key.
  • Переосмислення стану у React: де насправді мають знаходитися ваші дані — У цій статті пояснюється, як зменшити кількість помилок у React шляхом розміщення стану в URL, DOM або похідних значень замість надмірного використання useState.