Розкрийте силу машин станів у React за допомогою власних хуків. Вивчайте абстрагування складної логіки, покращуйте супроводжуваність коду та створюйте надійні додатки.
React Custom Hook State Machine: Оволодіння Абстрагуванням Складної Логіки Стану
У міру зростання складності React-застосунків, керування станом може стати значним викликом. Традиційні підходи, що використовують `useState` та `useEffect`, можуть швидко призвести до заплутаної логіки та важкого для підтримки коду, особливо при роботі зі складними переходами станів та побічними ефектами. Саме тут на допомогу приходять машини станів, а саме власні React-хуки, які їх реалізують. Ця стаття проведе вас через концепцію машин станів, продемонструє, як їх реалізувати як власні хуки в React, та проілюструє переваги, які вони пропонують для створення масштабованих та підтримуваних додатків для глобальної аудиторії.
Що таке машина станів?
Машина станів (або скінченна машина станів, FSM) – це математична модель обчислення, яка описує поведінку системи, визначаючи скінченну кількість станів та переходи між цими станами. Думайте про це як про блок-схему, але зі строгішими правилами та більш формальним визначенням. Основні поняття включають:
- Стани: Представляють різні умови або фази системи.
- Переходи: Визначають, як система переходить з одного стану в інший на основі певних подій або умов.
- Події: Тригери, які спричиняють переходи станів.
- Початковий стан: Стан, з якого починається система.
Машини станів чудово моделюють системи з чітко визначеними станами та чіткими переходами. Приклади рясніють у реальних сценаріях:
- Світлофори: Цикл через стани, такі як Червоний, Жовтий, Зелений, з переходами, що спрацьовують таймерами. Це глобально впізнаваний приклад.
- Обробка замовлень: Замовлення електронної комерції може переходити через стани, такі як "В очікуванні", "В обробці", "Відправлено" та "Доставлено". Це застосовується універсально до онлайн-роздрібної торгівлі.
- Потік аутентифікації: Процес аутентифікації користувача може включати такі стани, як "Не ввійшов", "Вхід", "Увійшов" та "Помилка". Протоколи безпеки зазвичай узгоджені в різних країнах.
Чому використовувати машини станів у React?
Інтеграція машин станів у ваші React-компоненти пропонує кілька вагомих переваг:
- Покращена організація коду: Машини станів забезпечують структурований підхід до керування станом, що робить ваш код більш передбачуваним і легшим для розуміння. Більше ніякого спагетті-коду!
- Зменшена складність: Чітко визначаючи стани та переходи, ви можете спростити складну логіку та уникнути ненавмисних побічних ефектів.
- Покращена тестуваність: Машини станів за своєю суттю є тестуваними. Ви можете легко перевірити, чи правильно працює ваша система, тестуючи кожен стан і перехід.
- Підвищена супроводжуваність: Декларативний характер машин станів полегшує зміну та розширення вашого коду в міру розвитку вашого додатку.
- Краща візуалізація: Існують інструменти, які можуть візуалізувати машини станів, надаючи чіткий огляд поведінки вашої системи, допомагаючи у співпраці та розумінні між командами з різними наборами навичок.
Реалізація машини станів як власного хука React
Давайте проілюструємо, як реалізувати машину станів за допомогою власного хука React. Ми створимо простий приклад кнопки, яка може бути в трьох станах: `idle`, `loading` та `success`. Кнопка починається в стані `idle`. При натисканні вона переходить у стан `loading`, імітує процес завантаження (використовуючи `setTimeout`), а потім переходить у стан `success`.
1. Визначте машину станів
Спочатку ми визначаємо стани та переходи нашої машини станів кнопки:
const buttonStateMachineDefinition = {
initial: 'idle',
states: {
idle: {
on: {
CLICK: 'loading',
},
},
loading: {
after: {
2000: 'success', // Через 2 секунди перейти до успіху
},
},
success: {},
},
};
Ця конфігурація використовує бібліотечно-незалежний (хоча й натхненний XState) підхід до визначення машини станів. Ми реалізуємо логіку для інтерпретації цього визначення самостійно у власному хуку. Властивість `initial` встановлює початковий стан у `idle`. Властивість `states` визначає можливі стани (`idle`, `loading` та `success`) та їхні переходи. Стан `idle` має властивість `on`, яка визначає перехід у стан `loading`, коли відбувається подія `CLICK`. Стан `loading` використовує властивість `after`, щоб автоматично перейти у стан `success` через 2000 мілісекунди (2 секунди). Стан `success` є термінальним станом у цьому прикладі.
2. Створіть власний хук
Тепер давайте створимо власний хук, який реалізує логіку машини станів:
import { useState, useEffect } from 'react';
const useStateMachine = (stateMachineDefinition) => {
const [currentState, setCurrentState] = useState(stateMachineDefinition.initial);
const [context, setContext] = useState({});
const transition = (event) => {
const stateDefinition = stateMachineDefinition.states[currentState];
if (stateDefinition && stateDefinition.on && stateDefinition.on[event]) {
setCurrentState(stateDefinition.on[event]);
}
};
useEffect(() => {
const stateDefinition = stateMachineDefinition.states[currentState];
if (stateDefinition && stateDefinition.after) {
const timeoutKeys = Object.keys(stateDefinition.after);
timeoutKeys.forEach(timeoutKey => {
const timeout = parseInt(timeoutKey, 10);
const nextState = stateDefinition.after[timeoutKey];
const timer = setTimeout(() => {
setCurrentState(nextState);
clearTimeout(timer);
}, timeout);
return () => clearTimeout(timer); // Очищення при розмонтуванні або зміні стану
});
}
}, [currentState, stateMachineDefinition.states]);
return {
currentState,
context,
transition,
};
};
export default useStateMachine;
Цей хук `useStateMachine` приймає визначення машини станів як аргумент. Він використовує `useState` для керування поточним станом та контекстом (ми пояснимо контекст пізніше). Функція `transition` приймає подію як аргумент і оновлює поточний стан на основі визначених переходів у визначенні машини станів. Хук `useEffect` обробляє властивість `after`, встановлюючи таймери для автоматичного переходу до наступного стану після вказаної тривалості. Хук повертає поточний стан, контекст та функцію `transition`.
3. Використовуйте власний хук у компоненті
Нарешті, давайте використаємо власний хук у React-компоненті:
import React from 'react';
import useStateMachine from './useStateMachine';
const buttonStateMachineDefinition = {
initial: 'idle',
states: {
idle: {
on: {
CLICK: 'loading',
},
},
loading: {
after: {
2000: 'success', // Через 2 секунди перейти до успіху
},
},
success: {},
},
};
const MyButton = () => {
const { currentState, transition } = useStateMachine(buttonStateMachineDefinition);
const handleClick = () => {
if (currentState === 'idle') {
transition('CLICK');
}
};
let buttonText = 'Натисніть мене';
if (currentState === 'loading') {
buttonText = 'Завантаження...';
} else if (currentState === 'success') {
buttonText = 'Успішно!';
}
return (
);
};
export default MyButton;
Цей компонент використовує хук `useStateMachine` для керування станом кнопки. Функція `handleClick` надсилає подію `CLICK`, коли натискають кнопку (і лише якщо вона знаходиться в стані `idle`). Компонент відображає різний текст залежно від поточного стану. Кнопка вимкнена під час завантаження, щоб запобігти кільком натисканням.
Обробка контексту в машинах станів
У багатьох реальних сценаріях машини станів повинні керувати даними, які зберігаються під час переходів станів. Ці дані називаються контекстом. Контекст дозволяє зберігати та оновлювати відповідну інформацію в міру проходження машини станів.
Давайте розширимо наш приклад кнопки, щоб включити лічильник, який збільшується щоразу, коли кнопка успішно завантажується. Ми змінимо визначення машини станів та власний хук для обробки контексту.
1. Оновіть визначення машини станів
const buttonStateMachineDefinition = {
initial: 'idle',
context: {
count: 0,
},
states: {
idle: {
on: {
CLICK: 'loading',
},
},
loading: {
after: {
2000: 'success',
},
},
success: {
entry: (context) => {
return { ...context, count: context.count + 1 };
},
},
},
};
Ми додали властивість `context` до визначення машини станів з початковим значенням `count` 0. Ми також додали дію `entry` до стану `success`. Дія `entry` виконується, коли машина станів входить у стан `success`. Вона приймає поточний контекст як аргумент і повертає новий контекст з збільшеним `count`. `entry` тут показує приклад зміни контексту. Оскільки об'єкти Javascript передаються за посиланням, важливо повернути *новий* об'єкт, а не змінювати вихідний.
2. Оновіть власний хук
import { useState, useEffect } from 'react';
const useStateMachine = (stateMachineDefinition) => {
const [currentState, setCurrentState] = useState(stateMachineDefinition.initial);
const [context, setContext] = useState(stateMachineDefinition.context || {});
const transition = (event) => {
const stateDefinition = stateMachineDefinition.states[currentState];
if (stateDefinition && stateDefinition.on && stateDefinition.on[event]) {
setCurrentState(stateDefinition.on[event]);
}
};
useEffect(() => {
const stateDefinition = stateMachineDefinition.states[currentState];
if(stateDefinition && stateDefinition.entry){
const newContext = stateDefinition.entry(context);
setContext(newContext);
}
if (stateDefinition && stateDefinition.after) {
const timeoutKeys = Object.keys(stateDefinition.after);
timeoutKeys.forEach(timeoutKey => {
const timeout = parseInt(timeoutKey, 10);
const nextState = stateDefinition.after[timeoutKey];
const timer = setTimeout(() => {
setCurrentState(nextState);
clearTimeout(timer);
}, timeout);
return () => clearTimeout(timer); // Очищення при розмонтуванні або зміні стану
});
}
}, [currentState, stateMachineDefinition.states, context]);
return {
currentState,
context,
transition,
};
};
export default useStateMachine;
Ми оновили хук `useStateMachine`, щоб ініціалізувати стан `context` з `stateMachineDefinition.context` або порожнім об'єктом, якщо контекст не надано. Ми також додали `useEffect` для обробки дії `entry`. Коли поточний стан має дію `entry`, ми виконуємо її та оновлюємо контекст повернутим значенням.
3. Використовуйте оновлений хук у компоненті
import React from 'react';
import useStateMachine from './useStateMachine';
const buttonStateMachineDefinition = {
initial: 'idle',
context: {
count: 0,
},
states: {
idle: {
on: {
CLICK: 'loading',
},
},
loading: {
after: {
2000: 'success',
},
},
success: {
entry: (context) => {
return { ...context, count: context.count + 1 };
},
},
},
};
const MyButton = () => {
const { currentState, context, transition } = useStateMachine(buttonStateMachineDefinition);
const handleClick = () => {
if (currentState === 'idle') {
transition('CLICK');
}
};
let buttonText = 'Натисніть мене';
if (currentState === 'loading') {
buttonText = 'Завантаження...';
} else if (currentState === 'success') {
buttonText = 'Успішно!';
}
return (
Лічильник: {context.count}
);
};
export default MyButton;
Тепер ми отримуємо доступ до `context.count` у компоненті та відображаємо його. Щоразу, коли кнопка успішно завантажується, лічильник збільшуватиметься.
Розширені концепції машин станів
Хоча наш приклад є відносно простим, машини станів можуть обробляти набагато складніші сценарії. Ось деякі розширені концепції, які слід враховувати:
- Охоронці: Умови, які повинні бути виконані для переходу. Наприклад, перехід може бути дозволений лише в тому випадку, якщо користувач аутентифікований або якщо певне значення даних перевищує поріг.
- Дії: Побічні ефекти, які виконуються при вході або виході зі стану. Вони можуть включати виклики API, оновлення DOM або надсилання подій іншим компонентам.
- Паралельні стани: Дозволяють моделювати системи з кількома одночасними діями. Наприклад, програвач відео може мати одну машину станів для елементів керування відтворенням (відтворення, пауза, зупинка) та іншу для керування якістю відео (низька, середня, висока).
- Ієрархічні стани: Дозволяють вкладати стани в інші стани, створюючи ієрархію станів. Це може бути корисним для моделювання складних систем з багатьма пов’язаними станами.
Альтернативні бібліотеки: XState та інші
Хоча наш власний хук забезпечує базову реалізацію машини станів, кілька чудових бібліотек можуть спростити процес і запропонувати більш розширені функції.
XState
XState – популярна JavaScript-бібліотека для створення, інтерпретації та виконання машин станів та діаграм станів. Вона надає потужний та гнучкий API для визначення складних машин станів, включаючи підтримку охоронців, дій, паралельних станів та ієрархічних станів. XState також пропонує чудові інструменти для візуалізації та налагодження машин станів.
Інші бібліотеки
Інші варіанти включають:
- Robot: Легка бібліотека керування станом з акцентом на простоту та продуктивність.
- react-automata: Бібліотека, спеціально розроблена для інтеграції машин станів у React-компоненти.
Вибір бібліотеки залежить від конкретних потреб вашого проекту. XState – хороший вибір для складних машин станів, тоді як Robot та react-automata підходять для більш простих сценаріїв.
Найкращі практики використання машин станів
Щоб ефективно використовувати машини станів у ваших React-додатках, враховуйте наступні найкращі практики:
- Почніть з малого: Почніть з простих машин станів і поступово збільшуйте складність за потреби.
- Візуалізуйте свою машину станів: Використовуйте інструменти візуалізації, щоб отримати чітке уявлення про поведінку вашої машини станів.
- Напишіть вичерпні тести: Ретельно протестуйте кожен стан і перехід, щоб переконатися, що ваша система працює правильно.
- Документуйте свою машину станів: Чітко документуйте стани, переходи, охоронців і дії вашої машини станів.
- Враховуйте інтернаціоналізацію (i18n): Якщо ваша програма орієнтована на глобальну аудиторію, переконайтеся, що логіка вашої машини станів та інтерфейс користувача належним чином інтернаціоналізовані. Наприклад, використовуйте окремі машини станів або контекст для обробки різних форматів дат або символів валют залежно від мови користувача.
- Доступність (a11y): Переконайтеся, що ваші переходи станів та оновлення інтерфейсу користувача доступні для користувачів з обмеженими можливостями. Використовуйте атрибути ARIA та семантичний HTML, щоб забезпечити належний контекст та зворотний зв’язок з допоміжними технологіями.
Висновок
Власні хуки React у поєднанні з машинами станів забезпечують потужний та ефективний підхід до керування складною логікою стану в React-додатках. Абстрагуючи переходи станів та побічні ефекти у добре визначену модель, ви можете покращити організацію коду, зменшити складність, покращити тестування та підвищити супроводжуваність. Незалежно від того, чи реалізуєте ви власний хук або використовуєте бібліотеку, як-от XState, включення машин станів у ваш робочий процес React може значно покращити якість і масштабованість ваших додатків для користувачів у всьому світі.