# Створення банківського додатку, частина 4: Концепції управління станом ## Тест перед лекцією [Тест перед лекцією](https://ff-quizzes.netlify.app/web/quiz/47) ## Вступ Управління станом схоже на навігаційну систему космічного апарату Voyager – коли все працює гладко, ви майже не помічаєте її. Але коли щось йде не так, це стає різницею між досягненням міжзоряного простору і безцільним дрейфом у космічній пустці. У веб-розробці стан представляє все, що ваш додаток повинен запам'ятати: статус входу користувача, дані форм, історію навігації та тимчасові стани інтерфейсу. Як ваш банківський додаток еволюціонував від простого форми входу до більш складного додатку, ви, ймовірно, зіткнулися з деякими поширеними проблемами. Оновіть сторінку, і користувачі несподівано виходять із системи. Закрийте браузер, і весь прогрес зникає. Виправляючи проблему, ви шукаєте через кілька функцій, які всі змінюють одні й ті ж дані різними способами. Це не ознаки поганого кодування – це природні труднощі, які виникають, коли додатки досягають певного рівня складності. Кожен розробник стикається з цими проблемами, коли їхні додатки переходять від "доказу концепції" до "готовності до виробництва". У цьому уроці ми впровадимо централізовану систему управління станом, яка перетворить ваш банківський додаток на надійний, професійний додаток. Ви навчитеся керувати потоками даних передбачувано, зберігати сеанси користувачів належним чином і створювати плавний користувацький досвід, який вимагають сучасні веб-додатки. ## Передумови Перед тим як зануритися в концепції управління станом, вам потрібно належним чином налаштувати середовище розробки та мати основу вашого банківського додатку. Цей урок безпосередньо базується на концепціях і коді з попередніх частин цієї серії. Переконайтеся, що у вас є наступні компоненти перед продовженням: **Необхідне налаштування:** - Завершіть [урок отримання даних](../3-data/README.md) - ваш додаток повинен успішно завантажувати і відображати дані облікового запису - Встановіть [Node.js](https://nodejs.org) на вашу систему для запуску бекенд API - Запустіть [сервер API](../api/README.md) локально для обробки операцій з даними облікового запису **Перевірка вашого середовища:** Переконайтеся, що ваш сервер API працює правильно, виконавши цю команду в терміналі: ```sh curl http://localhost:5000/api # -> should return "Bank API v1.0.0" as a result ``` **Що робить ця команда:** - **Надсилає** GET-запит до вашого локального сервера API - **Тестує** з'єднання і перевіряє, чи сервер відповідає - **Повертає** інформацію про версію API, якщо все працює правильно --- ## Діагностика поточних проблем зі станом Як Шерлок Холмс, який досліджує місце злочину, нам потрібно зрозуміти, що саме відбувається в нашій поточній реалізації, перш ніж ми зможемо вирішити загадку зникаючих сеансів користувачів. Проведемо простий експеримент, який виявить основні проблеми управління станом: **🧪 Спробуйте цей діагностичний тест:** 1. Увійдіть у ваш банківський додаток і перейдіть на панель управління 2. Оновіть сторінку браузера 3. Спостерігайте, що відбувається з вашим статусом входу Якщо вас перенаправляють назад на екран входу, ви виявили класичну проблему збереження стану. Ця поведінка виникає через те, що наша поточна реалізація зберігає дані користувача у змінних JavaScript, які скидаються при кожному завантаженні сторінки. **Проблеми поточної реалізації:** Проста змінна `account` з нашого [попереднього уроку](../3-data/README.md) створює три значні проблеми, які впливають як на користувацький досвід, так і на підтримку коду: | Проблема | Технічна причина | Вплив на користувача | |---------|--------|----------------| | **Втрачений сеанс** | Оновлення сторінки очищає змінні JavaScript | Користувачі повинні часто повторно аутентифікуватися | | **Розкидані оновлення** | Кілька функцій змінюють стан безпосередньо | Виправлення помилок стає дедалі складнішим | | **Неповне очищення** | Вихід із системи не очищає всі посилання на стан | Потенційні проблеми безпеки та конфіденційності | **Архітектурна проблема:** Як і розділений дизайн Титаніка, який здавався надійним, поки кілька відсіків не затопило одночасно, вирішення цих проблем окремо не вирішить основну архітектурну проблему. Нам потрібне комплексне рішення для управління станом. > 💡 **Що ми насправді намагаємося досягти тут?** [Управління станом](https://en.wikipedia.org/wiki/State_management) насправді полягає у вирішенні двох фундаментальних завдань: 1. **Де мої дані?**: Відстеження, яку інформацію ми маємо і звідки вона надходить 2. **Чи всі на одній хвилі?**: Переконання, що те, що бачать користувачі, відповідає тому, що насправді відбувається **Наш план дій:** Замість того, щоб бігати по колу, ми створимо **централізовану систему управління станом**. Уявіть це як одну дуже організовану людину, яка відповідає за всі важливі речі: ![Схема, що показує потоки даних між HTML, діями користувача та станом](../../../../translated_images/data-flow.fa2354e0908fecc89b488010dedf4871418a992edffa17e73441d257add18da4.uk.png) **Розуміння цього потоку даних:** - **Централізує** весь стан додатку в одному місці - **Спрямовує** всі зміни стану через контрольовані функції - **Гарантує**, що UI залишається синхронізованим з поточним станом - **Забезпечує** чіткий, передбачуваний шаблон для управління даними > 💡 **Професійна порада**: Цей урок зосереджений на фундаментальних концепціях. Для складних додатків бібліотеки, такі як [Redux](https://redux.js.org), пропонують більш розширені функції управління станом. Розуміння цих основних принципів допоможе вам освоїти будь-яку бібліотеку управління станом. > ⚠️ **Складна тема**: Ми не будемо розглядати автоматичні оновлення UI, які викликаються змінами стану, оскільки це включає концепції [реактивного програмування](https://en.wikipedia.org/wiki/Reactive_programming). Розгляньте це як чудовий наступний крок для вашого навчального шляху! ### Завдання: Централізація структури стану Давайте почнемо перетворювати наше розкидане управління станом у централізовану систему. Цей перший крок створює основу для всіх наступних покращень. **Крок 1: Створіть центральний об'єкт стану** Замініть просте оголошення `account`: ```js let account = null; ``` На структурований об'єкт стану: ```js let state = { account: null }; ``` **Чому ця зміна важлива:** - **Централізує** всі дані додатку в одному місці - **Готує** структуру для додавання більше властивостей стану пізніше - **Створює** чітку межу між станом і іншими змінними - **Встановлює** шаблон, який масштабується разом із зростанням вашого додатку **Крок 2: Оновіть шаблони доступу до стану** Оновіть ваші функції для використання нової структури стану: **У функціях `register()` і `login()`**, замініть: ```js account = ... ``` На: ```js state.account = ... ``` **У функції `updateDashboard()`**, додайте цей рядок на початку: ```js const account = state.account; ``` **Що досягають ці оновлення:** - **Зберігають** існуючу функціональність, покращуючи структуру - **Готують** ваш код до більш складного управління станом - **Створюють** послідовні шаблони для доступу до даних стану - **Встановлюють** основу для централізованих оновлень стану > 💡 **Примітка**: Це рефакторинг не вирішує наші проблеми негайно, але створює необхідну основу для потужних покращень, які будуть далі! ## Реалізація контрольованих оновлень стану З централізованим станом наступним кроком є встановлення контрольованих механізмів для модифікації даних. Цей підхід забезпечує передбачувані зміни стану та полегшує виправлення помилок. Основний принцип схожий на управління повітряним рухом: замість того, щоб дозволяти кільком функціям незалежно змінювати стан, ми будемо спрямовувати всі зміни через одну контрольовану функцію. Цей шаблон забезпечує чіткий контроль над тим, коли і як відбуваються зміни даних. **Управління незмінним станом:** Ми будемо розглядати наш об'єкт `state` як [*незмінний*](https://en.wikipedia.org/wiki/Immutable_object), тобто ми ніколи не будемо змінювати його безпосередньо. Натомість кожна зміна створює новий об'єкт стану з оновленими даними. Хоча цей підхід може спочатку здатися неефективним порівняно з прямими модифікаціями, він забезпечує значні переваги для виправлення помилок, тестування та підтримки передбачуваності додатку. **Переваги управління незмінним станом:** | Перевага | Опис | Вплив | |---------|-------------|--------| | **Передбачуваність** | Зміни відбуваються лише через контрольовані функції | Легше виправляти помилки та тестувати | | **Відстеження історії** | Кожна зміна стану створює новий об'єкт | Дозволяє функціональність скасування/повторення | | **Запобігання побічним ефектам** | Ніяких випадкових модифікацій | Запобігає загадковим помилкам | | **Оптимізація продуктивності** | Легко визначити, коли стан дійсно змінився | Дозволяє ефективні оновлення UI | **Незмінність JavaScript за допомогою `Object.freeze()`:** JavaScript надає [`Object.freeze()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) для запобігання модифікаціям об'єкта: ```js const immutableState = Object.freeze({ account: userData }); // Any attempt to modify immutableState will throw an error ``` **Розбір того, що тут відбувається:** - **Запобігає** прямим присвоєнням або видаленням властивостей - **Викидає** винятки, якщо спроби модифікації здійснюються - **Гарантує**, що зміни стану повинні проходити через контрольовані функції - **Створює** чіткий контракт для того, як стан може бути оновлений > 💡 **Глибоке занурення**: Дізнайтеся про різницю між *поверхневими* і *глибокими* незмінними об'єктами в [документації MDN](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#What_is_shallow_freeze). Розуміння цієї різниці є важливим для складних структур стану. ### Завдання Давайте створимо нову функцію `updateState()`: ```js function updateState(property, newData) { state = Object.freeze({ ...state, [property]: newData }); } ``` У цій функції ми створюємо новий об'єкт стану і копіюємо дані з попереднього стану за допомогою [*оператора розгортання (`...`)*](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals). Потім ми перевизначаємо певну властивість об'єкта стану новими даними за допомогою [нотації квадратних дужок](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Working_with_Objects#Objects_and_properties) `[property]` для присвоєння. Нарешті, ми блокуємо об'єкт, щоб запобігти модифікаціям за допомогою `Object.freeze()`. Зараз у нас є лише властивість `account`, збережена в стані, але з цим підходом ви можете додати стільки властивостей, скільки вам потрібно. Ми також оновимо ініціалізацію `state`, щоб переконатися, що початковий стан також заморожений: ```js let state = Object.freeze({ account: null }); ``` Після цього оновіть функцію `register`, замінивши присвоєння `state.account = result;` на: ```js updateState('account', result); ``` Зробіть те ж саме з функцією `login`, замінивши `state.account = data;` на: ```js updateState('account', data); ``` Тепер ми скористаємося можливістю виправити проблему, коли дані облікового запису не очищаються, коли користувач натискає *Logout*. Створіть нову функцію `logout()`: ```js function logout() { updateState('account', null); navigate('/login'); } ``` У `updateDashboard()` замініть перенаправлення `return navigate('/login');` на `return logout();`; Спробуйте зареєструвати новий обліковий запис, вийти з системи і знову увійти, щоб переконатися, що все працює правильно. > Порада: ви можете переглянути всі зміни стану, додавши `console.log(state)` в кінці `updateState()` і відкривши консоль у засобах розробки вашого браузера. ## Реалізація збереження даних Проблема втрати сеансу, яку ми визначили раніше, вимагає рішення для збереження, яке підтримує стан користувача між сеансами браузера. Це перетворює наш додаток з тимчасового досвіду на надійний, професійний інструмент. Подумайте, як атомні годинники підтримують точний час навіть під час відключення живлення, зберігаючи критичний стан у невипаровуваній пам'яті. Аналогічно, веб-додатки потребують механізмів збереження, щоб зберігати важливі дані користувача між сеансами браузера і оновленнями сторінки. **Стратегічні питання для збереження даних:** Перед реалізацією збереження врахуйте ці критичні фактори: | Питання | Контекст банківського додатку | Вплив рішення | |----------|-------------------|----------------| | **Чи є дані чутливими?** | Баланс рахунку, історія транзакцій | Вибір безпечних методів збереження | | **Як довго вони повинні зберігатися?** | Стан входу проти тимчасових налаштувань UI | Вибір відповідної тривалості збереження | | **Чи потрібні вони серверу?** | Токени аутентифікації проти налаштувань UI | Визначення вимог до спільного використання | **Опції збереження в браузері:** Сучасні браузери надають кілька механізмів збереження, кожен з яких призначений для різних випадків використання: **Основні API збереження:** 1. **[`localStorage`](https://developer.mozilla.org/docs/Web/API/Window/localStorage)**: Постійне [збереження ключ/значення](https://en.wikipedia.org/wiki/Key%E2%80%93value_database) - **Зберігає** дані між сеансами браузера безстрок > 💡 **Розширений варіант**: Для складних офлайн-додатків із великими наборами даних розгляньте використання [`IndexedDB` API](https://developer.mozilla.org/docs/Web/API/IndexedDB_API). Це повноцінна база даних на стороні клієнта, але її реалізація потребує більше зусиль. ### Завдання: Реалізувати збереження даних у localStorage Давайте реалізуємо збереження даних, щоб користувачі залишалися авторизованими, поки вони самі не вийдуть із системи. Ми використаємо `localStorage` для збереження даних облікового запису між сесіями браузера. **Крок 1: Визначення конфігурації збереження** ```js const storageKey = 'savedAccount'; ``` **Що забезпечує ця константа:** - **Створює** послідовний ідентифікатор для наших збережених даних - **Запобігає** помилкам у посиланнях на ключі збереження - **Полегшує** зміну ключа збереження за потреби - **Дотримується** найкращих практик для підтримуваного коду **Крок 2: Додати автоматичне збереження** Додайте цей рядок у кінці функції `updateState()`: ```js localStorage.setItem(storageKey, JSON.stringify(state.account)); ``` **Розбір того, що тут відбувається:** - **Конвертує** об'єкт облікового запису в JSON-рядок для збереження - **Зберігає** дані за допомогою нашого послідовного ключа збереження - **Виконується** автоматично при кожній зміні стану - **Гарантує**, що збережені дані завжди синхронізовані з поточним станом > 💡 **Перевага архітектури**: Оскільки ми централізували всі оновлення стану через `updateState()`, додавання збереження потребувало лише одного рядка коду. Це демонструє силу хороших архітектурних рішень! **Крок 3: Відновлення стану при завантаженні додатка** Створіть функцію ініціалізації для відновлення збережених даних: ```js function init() { const savedAccount = localStorage.getItem(storageKey); if (savedAccount) { updateState('account', JSON.parse(savedAccount)); } // Our previous initialization code window.onpopstate = () => updateRoute(); updateRoute(); } init(); ``` **Розуміння процесу ініціалізації:** - **Отримує** будь-які раніше збережені дані облікового запису з localStorage - **Парсить** JSON-рядок назад у об'єкт JavaScript - **Оновлює** стан за допомогою нашої контрольованої функції оновлення - **Відновлює** сесію користувача автоматично при завантаженні сторінки - **Виконується** перед оновленням маршруту, щоб забезпечити доступність стану **Крок 4: Оптимізація маршруту за замовчуванням** Оновіть маршрут за замовчуванням, щоб скористатися перевагами збереження: У `updateRoute()` замініть: ```js // Replace: return navigate('/login'); return navigate('/dashboard'); ``` **Чому це зміна має сенс:** - **Ефективно використовує** нашу нову систему збереження - **Дозволяє** панелі керування перевіряти автентифікацію - **Перенаправляє** на сторінку входу автоматично, якщо немає збереженої сесії - **Створює** більш плавний досвід користувача **Тестування вашої реалізації:** 1. Увійдіть у свій банківський додаток 2. Оновіть сторінку браузера 3. Переконайтеся, що ви залишаєтеся авторизованими і на панелі керування 4. Закрийте та знову відкрийте браузер 5. Перейдіть назад до вашого додатка і переконайтеся, що ви все ще авторизовані 🎉 **Досягнення розблоковано**: Ви успішно реалізували управління станом із збереженням! Ваш додаток тепер працює як професійний веб-додаток. ## Балансування збереження даних із їх актуальністю Наша система збереження успішно підтримує сесії користувачів, але вводить нову проблему: застарілі дані. Коли кілька користувачів або додатків змінюють ті самі дані на сервері, локальна кешована інформація стає неактуальною. Ця ситуація схожа на навігаторів вікінгів, які покладалися як на збережені зоряні карти, так і на поточні спостереження за небом. Карти забезпечували послідовність, але навігатори потребували свіжих спостережень, щоб враховувати змінні умови. Так само наш додаток потребує як збереженого стану користувача, так і актуальних даних із сервера. **🧪 Виявлення проблеми застарілих даних:** 1. Увійдіть у панель керування, використовуючи обліковий запис `test` 2. Виконайте цю команду в терміналі, щоб змоделювати транзакцію з іншого джерела: ```sh curl --request POST \ --header "Content-Type: application/json" \ --data "{ \"date\": \"2020-07-24\", \"object\": \"Bought book\", \"amount\": -20 }" \ http://localhost:5000/api/accounts/test/transactions ``` 3. Оновіть сторінку панелі керування у браузері 4. Перевірте, чи бачите ви нову транзакцію **Що демонструє цей тест:** - **Показує**, як localStorage може стати "застарілим" (неактуальним) - **Моделює** реальні сценарії, де зміни даних відбуваються поза вашим додатком - **Виявляє** напруженість між збереженням і актуальністю даних **Проблема застарілих даних:** | Проблема | Причина | Вплив на користувача | |----------|---------|----------------------| | **Застарілі дані** | localStorage ніколи не закінчується автоматично | Користувачі бачать неактуальну інформацію | | **Зміни на сервері** | Інші додатки/користувачі змінюють ті самі дані | Непослідовні вигляди на різних платформах | | **Кеш проти реальності** | Локальний кеш не відповідає стану сервера | Поганий досвід користувача і плутанина | **Стратегія вирішення:** Ми реалізуємо патерн "оновлення при завантаженні", який балансує переваги збереження з потребою в актуальних даних. Цей підхід підтримує плавний досвід користувача, забезпечуючи точність даних. ### Завдання: Реалізувати систему оновлення даних Ми створимо систему, яка автоматично отримує актуальні дані з сервера, зберігаючи переваги нашого управління станом із збереженням. **Крок 1: Створення оновлювача даних облікового запису** ```js async function updateAccountData() { const account = state.account; if (!account) { return logout(); } const data = await getAccount(account.user); if (data.error) { return logout(); } updateState('account', data); } ``` **Розуміння логіки цієї функції:** - **Перевіряє**, чи користувач наразі авторизований (існує state.account) - **Перенаправляє** на вихід із системи, якщо немає дійсної сесії - **Отримує** актуальні дані облікового запису з сервера за допомогою існуючої функції `getAccount()` - **Обробляє** помилки сервера, завершуючи недійсні сесії - **Оновлює** стан актуальними даними за допомогою нашої контрольованої системи оновлення - **Запускає** автоматичне збереження в localStorage через функцію `updateState()` **Крок 2: Створення обробника оновлення панелі керування** ```js async function refresh() { await updateAccountData(); updateDashboard(); } ``` **Що виконує ця функція оновлення:** - **Координує** процес оновлення даних і оновлення інтерфейсу - **Очікує**, поки актуальні дані будуть завантажені, перед оновленням відображення - **Гарантує**, що панель керування показує найактуальнішу інформацію - **Підтримує** чітке розділення між управлінням даними та оновленням інтерфейсу **Крок 3: Інтеграція з системою маршрутів** Оновіть конфігурацію маршруту, щоб автоматично запускати оновлення: ```js const routes = { '/login': { templateId: 'login' }, '/dashboard': { templateId: 'dashboard', init: refresh } }; ``` **Як працює ця інтеграція:** - **Виконує** функцію оновлення щоразу, коли завантажується маршрут панелі керування - **Гарантує**, що актуальні дані завжди відображаються, коли користувачі переходять до панелі керування - **Підтримує** існуючу структуру маршруту, додаючи актуальність даних - **Забезпечує** послідовний патерн для ініціалізації, специфічної для маршруту **Тестування вашої системи оновлення даних:** 1. Увійдіть у свій банківський додаток 2. Виконайте команду curl, яку ми використовували раніше, щоб створити нову транзакцію 3. Оновіть сторінку панелі керування або перейдіть на інший маршрут і назад 4. Переконайтеся, що нова транзакція з'являється негайно 🎉 **Досягнуто ідеального балансу**: Ваш додаток тепер поєднує плавний досвід збереження стану з точністю актуальних даних із сервера! ## Виклик GitHub Copilot Agent 🚀 Використовуйте режим Agent, щоб виконати наступний виклик: **Опис:** Реалізуйте комплексну систему управління станом із функціональністю скасування/повторення для банківського додатка. Цей виклик допоможе вам практикувати розширені концепції управління станом, включаючи відстеження історії стану, незмінні оновлення та синхронізацію з інтерфейсом користувача. **Підказка:** Створіть вдосконалену систему управління станом, яка включає: 1) Масив історії стану, що відстежує всі попередні стани, 2) Функції скасування та повторення, які можуть повернутися до попередніх станів, 3) Кнопки інтерфейсу для операцій скасування/повторення на панелі керування, 4) Максимальний ліміт історії у 10 станів, щоб уникнути проблем із пам'яттю, і 5) Правильне очищення історії при виході користувача. Переконайтеся, що функціональність скасування/повторення працює зі змінами балансу облікового запису і зберігається між оновленнями браузера. Дізнайтеся більше про [режим Agent](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode) тут. ## 🚀 Виклик: Оптимізація збереження Ваша реалізація тепер ефективно обробляє сесії користувачів, оновлення даних і управління станом. Однак подумайте, чи наш поточний підхід оптимально балансує ефективність збереження з функціональністю. Як шахові майстри, які розрізняють важливі фігури та другорядні пішаки, ефективне управління станом вимагає визначення, які дані повинні зберігатися, а які завжди мають бути актуальними з сервера. **Аналіз оптимізації:** Оцініть вашу поточну реалізацію localStorage і розгляньте ці стратегічні питання: - Яка мінімальна інформація потрібна для підтримки автентифікації користувача? - Які дані змінюються настільки часто, що локальне кешування не приносить користі? - Як оптимізація збереження може покращити продуктивність без погіршення досвіду користувача? **Стратегія реалізації:** - **Визначте** основні дані, які повинні зберігатися (ймовірно, лише ідентифікація користувача) - **Модифікуйте** вашу реалізацію localStorage, щоб зберігати лише критичні дані сесії - **Гарантуйте**, що актуальні дані завжди завантажуються з сервера при відвідуванні панелі керування - **Перевірте**, що ваш оптимізований підхід зберігає той самий досвід користувача **Розширений розгляд:** - **Порівняйте** компроміси між збереженням повних даних облікового запису та лише токенів автентифікації - **Документуйте** ваші рішення та аргументи для майбутніх членів команди Цей виклик допоможе вам мислити як професійний розробник, який враховує як досвід користувача, так і ефективність додатка. Не поспішайте експериментувати з різними підходами! ## Післялекційний тест [Післялекційний тест](https://ff-quizzes.netlify.app/web/quiz/48) ## Завдання [Реалізувати діалогове вікно "Додати транзакцію"](assignment.md) Ось приклад результату після виконання завдання: ![Скріншот, що показує приклад діалогового вікна "Додати транзакцію"](../../../../translated_images/dialog.93bba104afeb79f12f65ebf8f521c5d64e179c40b791c49c242cf15f7e7fab15.uk.png) --- **Відмова від відповідальності**: Цей документ був перекладений за допомогою сервісу автоматичного перекладу [Co-op Translator](https://github.com/Azure/co-op-translator). Хоча ми прагнемо до точності, будь ласка, майте на увазі, що автоматичні переклади можуть містити помилки або неточності. Оригінальний документ на його рідній мові слід вважати авторитетним джерелом. Для критичної інформації рекомендується професійний людський переклад. Ми не несемо відповідальності за будь-які непорозуміння або неправильні тлумачення, що виникають внаслідок використання цього перекладу.