Отключете силата на машините на състоянията в React с персонализирани хукове. Научете се да абстрахирате сложна логика, да подобрите поддръжката на кода и да изграждате стабилни приложения.
Персонализиран хук за машина на състоянията в React: Овладяване на абстракцията на сложна логика на състоянията
С нарастването на сложността на 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', // After 2 seconds, transition to success
},
},
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); // Cleanup on unmount or state change
});
}
}, [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', // After 2 seconds, transition to success
},
},
success: {},
},
};
const MyButton = () => {
const { currentState, transition } = useStateMachine(buttonStateMachineDefinition);
const handleClick = () => {
if (currentState === 'idle') {
transition('CLICK');
}
};
let buttonText = 'Click Me';
if (currentState === 'loading') {
buttonText = 'Loading...';
} else if (currentState === 'success') {
buttonText = 'Success!';
}
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); // Cleanup on unmount or state change
});
}
}, [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 = 'Click Me';
if (currentState === 'loading') {
buttonText = 'Loading...';
} else if (currentState === 'success') {
buttonText = 'Success!';
}
return (
Count: {context.count}
);
};
export default MyButton;
Сега имаме достъп до `context.count` в компонента и го показваме. Всеки път, когато бутонът се зареди успешно, броячът ще се увеличи.
Разширени концепции за машини на състоянията
Въпреки че нашият пример е сравнително прост, машините на състоянията могат да се справят с много по-сложни сценарии. Ето някои разширени концепции, които да вземете предвид:
- Предпазители (Guards): Условия, които трябва да бъдат изпълнени, за да се осъществи преход. Например, преход може да бъде разрешен само ако потребителят е удостоверен или ако определена стойност на данни надвишава праг.
- Действия (Actions): Странични ефекти, които се изпълняват при влизане или излизане от състояние. Те могат да включват извършване на 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 може значително да подобри качеството и мащабируемостта на вашите приложения за потребители по целия свят.