Усі ми знаємо, що програмувати й писати код можна і без глибокого розуміння концепцій мови або взагалі програмування. Це, звісно, в більшості випадків трохи сумно, але водночас це означає, що є куди рости — і це чудово. Ви ж можете добре і безпечно для себе й інших керувати автомобілем, не розуміючи як саме цей автомобіль працює всередині, правда?
На роботі чи під час навчання ми можемо виконувати величезну кількість повсякденної роботи, не маючи глибоких знань, але якщо ми хочемо користуватися мовою, якою пишемо, на максимум, то все ж треба копати глибше:) Саме цим ми сьогодні й будемо займатися.
Сьогодні ми трошки позаграємо з тим, що робить людям багато головного болю зазвичай — зі складними обчисленнями. Звучить інтригуючи, чи не так?
Переходячи ближче до теми, хочу сказати, що я особисто не зустрічав багато інформації на цю тему. З цього можу зробити висновок, що знаємо ми про автоматне програмування явно недостатньо.
КІНЦЕВІ АВТОМАТИ
Почнемо з того, що таке кінцеві автомати (Finite State Machines). Кінцевий автомат — це математична абстракція, яка використовується для розробки алгоритмів. Про алгоритми ви можете дізнатися трохи більше з моєї статті «O(n) або складність алгоритмів».
І з того, що написано вище, можна вирішити, що автоматне програмування — це технічна тема, яка більше підходить для математиків, ніж для фронтенд-розробників.
Проте, як тільки ви почнете помічати кінцеві автомати у вашому коді, стане зрозуміло, що вони — ключовий інструмент для створення динамічних і взаємодійних інтерфейсів.
Стан і його зміни — це те, що лежить в основі роботи більшості фронтенд-додатків. Від кнопок до модальних вікон і складних ієрархій станів — кінцеві автомати забезпечують чітку та зрозумілу структуру для управління цими процесами.
Опис роботи кінцевого автомата — це зазвичай щось типу:
- автомат зчитує серію входів;
- коли він зчитує вхід, він переходить в інший стан;
- кожен стан визначає, у який стан переходити для даного входу.
І це звучить явно складно, але насправді все досить просто. Розберімося детальніше.
ПРОСТІШЕ ПРО КІНЦЕВІ ЗАПИТИ
Кінцеві автомати — це математична модель, яка описує систему зі скінченною кількістю станів і чітко визначеними правилами переходу між ними. У програмуванні кінцеві автомати використовуються для управління станами об’єктів, компонентів або систем у відповідь на певні події.
У кінцевого автомата є свої компоненти, ось основні з них:
- Стан (state): це ми всі знаємо що таке. Найбанальніший приклад стану: кнопка може бути в стані «натиснута» або «відпущена».
- Початковий стан (initial state): стан, у якому знаходиться автомат на початку роботи.
- Подія (event): тригери, які викликають зміну стану. Наприклад, натискання кнопки.
- Перехід (transition): визначення, як подія переводить автомат з одного стану в інший.
- Кінцевий стан (final state): опціональний стан, який вказує на завершення роботи автомата.
Погодьтеся, поки що все звучить достатньо просто і зрозуміло, так?
ПЕРЕХІД ДО АВТОМАТНОГО ПРОГРАМУВАННЯ
У цій статті ми розглянемо, як кінцеві автомати допомагають впорядковувати стани в React і загалом у фронтенді, які підходи можна використовувати для роботи зі станами, і чому чітке управління станом — це шлях до ефективного, масштабованого й надійного коду.
Тема кінцевих автоматів займає центральне місце у фронтенд-розробці. Інтерактивні елементи завжди пов’язані з процесами, які стосуються зміни станів.
Модальні вікна можуть бути відкритими чи прихованими, кнопка може бути натиснута, відпущена або заблокована (наприклад, під час AJAX-запиту). Прикладів безліч.
Нерідко ці автомати залежать один від одного, що створює ієрархію автоматів. Наприклад, можливість взаємодії з елементом на екрані може з’явитися тільки після натискання кнопки «редагувати».
У React робота з автоматами проста до смішного і в більшості випадків не вимагає використання спеціальних бібліотек. Візьмемо, наприклад, кнопку, яка відповідає за показ тексту. Її стани можна описати так:
- за замовчуванням текст приховано (стан hidden);
- клік по кнопці відображає текст (стан shown);
- повторний клік приховує текст (hidden).
У цьому випадку кнопка має два стани, тому можна спростити задачу і використовувати прапорець як індикатор стану. Назвемо його isShown. Використовувати прапорці з булевими значеннями дуже не рекомендується у бекенді, коли стан зберігається у базі даних.
Ціна зміни автомата занадто висока (наприклад, зміна типу колонки з boolean на string), тому навіть у випадку бінарної логіки краще використовувати повноцінний автомат із названими станами. Іншими словами, для зберігання стану використовуйте не булеве поле (true/false), а текстове поле, у якому буде зберігатися повна назва стану.
Наприклад, якщо стаття може перебувати у двох станах («опублікована» або «не опублікована»), то замість поля published: bool зі значеннями true та false слід використовувати поле publishing_state зі значеннями published і unpublished.
import React from "react";
class Component extends React.Component {
constructor(props) {
super(props);
this.state = { isShown: false };
}
toggleText = () => {
const { isShown } = this.state;
this.setState({ isShown: !isShown });
};
render() {
const { isShown } = this.state;
return (
<div>
<button onClick={this.toggleText} type="button">
{isShown ? 'hide' : 'show'}
</button>
{isShown && <p>🌟</p>}
</div>
);
}
}
export default Component;
Більшість коду в React (як і у всьому фронтенді) виглядає саме так, як у прикладі вище. Події породжують зміни стану в даних, на основі яких змінюється представлення.
Кількість кінцевих автоматів у фронтенд-додатках зростає з астрономічною швидкістю, головне — їх бачити та явно виділяти.
СТРУКТУРА СТАНУ
Ні для кого не секрет, що дані, з якими працює React, як правило, надходять із бекенду. І ці дані також беруть участь у різних процесах і перебувають у різних станах.
Наприклад, стаття може бути опублікована, а може й ні. І залежно від того, у якому вона стані, формується UI.
І тут починається найцікавіше. Конкретно стан опублікованості статті не є частиною UI, але UI використовує цей стан і вразі зміни він синхронізується між фронтендом і бекендом. Але в UI часто з’являються стани, які відповідають виключно за зовнішній вигляд, але не є частиною даних.
Тут, звісно, можна трошки заплутатися, але далі ми будемо «розплутувати»:)
Якщо припустити, що дані, отримані з бекенду, всередині нашого об’єкта стану зберігаються як список під ключем items, виникає питання: куди записувати дані, які відповідають за стан UI?
Тобто ті стани, які з’являються тільки при взаємодії з користувачем і не використовуються на серверній стороні?
Приклад: з бекенду приходить стаття такої структури:
{ "id": 3, "name": "How to program", "state": "published" }
Вона потрапляє до items. А в UI є можливість зайти в її редагування, і для цього використовується прапорець (стан) isEditing, який існує лише на екрані.
Питання: де зберігати цю змінну?
Найпростіший варіант: змінити саму статтю всередині items, щоб вона мала такий вигляд:
{ "id": 3, "name": "How to program", "state": "published", "isEditing": true }
Хоча, на перший погляд, це здається розумним, проблеми, які це створює, перевищують користь. Здебільшого ці проблеми пов’язані із задачами синхронізації.
Іноді потрібно відправити всю статтю на сервер (після змін), а іноді «перечитати» її з бекенду. У такій ситуації потрібно або вилучати лише потрібні дані, або постійно робити «мердж» (об’єднання), щоб не втратити стан UI.
Практика показала, що набагато простіше додавати окремий список виключно для задач зберігання стану UI. Тобто в state з’явиться список під назвою itemUIStates, і для статті в нього додається елемент:
{ "articleId": 3, "isEditing": true }
СПРОЩЕННЯ РОБОТИ З КІНЦЕВИМИ АВТОМАТАМИ
Наразі однією з найпопулярніших бібліотек для роботи з кінцевими автоматами є бібліотека XState. Вона допомагає структурувати стани вашого додатка, зробити їх зрозумілими, передбачуваними й легко керованими. Більше про неї можна почитати тут, і також є окрема версія для роботи з React, що дуже зручно.
Де використовується XState?
- Інтерфейси користувача: управління станами модальних вікон, форм, випадаючих меню тощо.
- Оркестрація задач: складні сценарії з послідовними, паралельними чи залежними завданнями.
- Ігрова логіка: робота зі станами персонажів чи об’єктів.
- Системи реального часу: чати, інтерактивні додатки, потокова обробка даних.
Приклад інтеграції XState з React :
import React from 'react';
import { useMachine } from '@xstate/react';
import { createMachine } from 'xstate';
const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: {
on: { TOGGLE: 'active' },
},
active: {
on: { TOGGLE: 'inactive' },
},
},
});
const ToggleButton = () => {
const [state, send] = useMachine(toggleMachine);
return (
<button onClick={() => send('TOGGLE')}>
{state.matches('inactive') ? 'Turn On' : 'Turn Off'}
</button>
);
};
export default ToggleButton;
ПЕРЕВАГИ:
- зрозуміле і передбачуване управління станами;
- масштабованість для складних систем;
- легкість тестування і дебагінгу;
- інтеграція з сучасними фреймворками.
НЕДОЛІКИ:
- навчання може зайняти час для початківців;
- само собою, у деяких простих випадках може бути «надмірною» для використання.
ВИСНОВОК
Отже, кінцеві автомати — це не просто математична абстракція, а корисний інструмент, який може значно спростити ваше життя як фронтенд-розробника.
Вони допомагають упорядкувати логіку, зробити код зрозумілішим і підвищити його стійкість до помилок.
Так, як і будь-яка концепція, кінцеві автомати можуть спочатку здатися складними (й вони в принципі не найпростіше, що може бути), але щойно ви почнете бачити їх у своєму коді, робота з ними стане такою ж природною, як натискання кнопки.
І, чесно кажучи, це зовсім не схоже на магію — це всього лише добре структурований підхід до управління станами.
Не бійтеся вчитися нового. Візьміть автоматне програмування на озброєння, спробуйте бібліотеку XState або хоча б розгляньте свої компоненти в контексті кінцевих автоматів. Результат, повірте, не змусить довго чекати.
Ну, а якщо поки що все здається складним — це нормально. Помиляйтесь, експериментуйте, грайтесь із кодом і автоматами, й колись це точно спрацює вам на руку.
А ми, як завжди, зустрінемось у цьому ж місці в той самий час:)