Защищенные переходы¶
Часто вам нужно, чтобы переход между состояниями происходил только при соблюдении определенных условий состояния (конечных или расширенных) или события. Например, предположим, что вы создаете машину для формы поиска и хотите, чтобы поиск был разрешен только в том случае, если:
- пользователю разрешен поиск (
.canSearch
в этом примере) - поисковый запрос
query
не пустой
Это хороший вариант использования для «защищенного перехода», который является переходом, который происходит только в том случае, если выполняется какое-то условие (cond
). Переход с условием называется защищенным переходом (guarded transition).
Защитные функции¶
Защитная функция (condition function) (также известная как защитник — guard), указанная в свойстве .cond
перехода в виде строки или объекта условия со свойством {type: '...'}
и принимает 3 параметра:
Параметр | Тип | Описание |
---|---|---|
context |
object | Контекст автомата |
event |
object | сработавшее событие |
condMeta |
object | мета-данные |
Объект condMeta
включает следующие свойства:
cond
— объект исходного состоянияstate
— состояние машины до перехода_event
— SCXML событие
Возвращает
true
или false
, что определяет, будет ли осуществлен переход.
const searchValid = (context, event) => {
return (
context.canSearch &&
event.query &&
event.query.length > 0
);
};
const searchMachine = createMachine(
{
id: 'search',
initial: 'idle',
context: {
canSearch: true,
},
states: {
idle: {
on: {
SEARCH: [
{
target: 'searching',
// Only transition to 'searching'
// if the guard (cond) evaluates to true
cond: searchValid, // or { type: 'searchValid' }
},
{ target: '.invalid' },
],
},
initial: 'normal',
states: {
normal: {},
invalid: {},
},
},
searching: {
entry: 'executeSearch',
// ...
},
searchError: {
// ...
},
},
},
{
guards: {
searchValid, // optional, if the implementation doesn't change
},
}
);
Перейдите на вкладку Events и отправьте событие типа {"type": "SEARCH", "query": "something"}
в визуализаторе:
https://stately.ai/viz?gist=09af23963bfa1767ce3900f2ae730029
Если cond
возвращает false
, то переход не будет выбран, и переход не будет происходить из этого узла состояния. Если все переходы в дочернем состоянии имеют защитные функции, которые возвращают false
и позволяют их выбрать, то событие будет передано в родительское состояние и обработано там.
Пример использования с контекстом context
:
import { interpret } from 'xstate';
const searchService = interpret(searchMachine)
.onTransition((state) => console.log(state.value))
.start();
searchService.send({ type: 'SEARCH', query: '' });
// => 'idle'
searchService.send({ type: 'SEARCH', query: 'something' });
// => 'searching'
Подсказка
Реализации защитных функций можно быстро прототипировать, задав свойство cond
прямо в конфигурации автомата:
// ...
SEARCH: {
target: 'searching',
cond: (context, event) => context.canSearch && event.query && event.query.length > 0
}
// ...
Сериализация защитных функций¶
Защитные функции могут (и должны) быть сериализованы как строка или объект со свойством {type: '...'}
. Детали реализации защитной функции указаны в свойстве guards
параметров автомата, где key
— это тип type
защитной функции (указанный как строка или объект), а значение — это функция, которая принимает три параметра:
context
— текущий контекст автоматаevent
— событие, которое вызвало потенциальный переход-
guardMeta
— объект мета-данных о защитной функции и переходе:-
cond
— исходный объектcond
,
-
state
— состояние автомата то потенциального перехода
Рефакторинг приведенного выше примера:
const searchMachine = createMachine(
{
// ...
states: {
idle: {
on: {
SEARCH: {
target: 'searching',
// The 'searchValid' guard implementation details are
// specified in the machine config
cond: 'searchValid', // or { type: 'searchValid' }
},
},
},
// ...
},
},
{
guards: {
searchValid: (context, event) => {
return (
context.canSearch &&
event.query &&
event.query.length > 0
);
},
},
}
);
Кастомизированные защитные функции¶
Начиная с версии 4.4+
Иногда, предпочтительнее сериализовать в JSON не только состояние переходов, но и логику защитных функций, определив её как объект с соответствующими данными:
const searchMachine = createMachine(
{
// ...
states: {
idle: {
on: {
SEARCH: {
target: 'searching',
// Custom guard object
cond: {
type: 'searchValid',
minQueryLength: 3,
},
},
},
},
// ...
},
},
{
guards: {
searchValid: (context, event, { cond }) => {
// cond === { type: 'searchValid', minQueryLength: 3 }
return (
context.canSearch &&
event.query &&
event.query.length > cond.minQueryLength
);
},
},
}
);
Несколько защитных функций¶
Если вы хотите, чтобы одно событие переходило в разные состояния в определенных ситуациях, вы можете предоставить массив условных переходов. Каждый переход будет проверяться по порядку, и будет выполнен первый переход, для которого защитная функция cond
вернет true
.
Например, вы можете смоделировать дверь, которая прослушивает событие OPEN
, переходит в состояние 'opened'
, если вы администратор, или переходит в состояние 'closed.error'
, если значение alert
истинно, или переходит в состояние 'closed.idle'
в противном случае.
import {
createMachine,
actions,
interpret,
assign,
} from 'xstate';
const doorMachine = createMachine(
{
id: 'door',
initial: 'closed',
context: {
level: 'user',
alert: false, // alert when intrusions happen
},
states: {
closed: {
initial: 'idle',
states: {
idle: {},
error: {},
},
on: {
SET_ADMIN: {
actions: assign({ level: 'admin' }),
},
SET_ALARM: {
actions: assign({ alert: true }),
},
OPEN: [
// Transitions are tested one at a time.
// The first valid transition will be taken.
{ target: 'opened', cond: 'isAdmin' },
{ target: '.error', cond: 'shouldAlert' },
{ target: '.idle' },
],
},
},
opened: {
on: {
CLOSE: { target: 'closed' },
},
},
},
},
{
guards: {
isAdmin: (context) => context.level === 'admin',
shouldAlert: (context) => context.alert === true,
},
}
);
const doorService = interpret(doorMachine)
.onTransition((state) => console.log(state.value))
.start();
// => { closed: 'idle' }
doorService.send({ type: 'OPEN' });
// => { closed: 'idle' }
doorService.send({ type: 'SET_ALARM' });
// => { closed: 'idle' }
// (state does not change, but context changes)
doorService.send({ type: 'OPEN' });
// => { closed: 'error' }
doorService.send({ type: 'SET_ADMIN' });
// => { closed: 'error' }
// (state does not change, but context changes)
doorService.send({ type: 'OPEN' });
// => 'opened'
// (since context.isAdmin === true)
Внимание
Функция cond
всегда должна быть чистой функцией, которая ссылается только на параметры контекста context
и события event
.
Подсказка
Не злоупотребляйте защитными условиями. Если что-то может быть представлено дискретно как два или более отдельных события вместо нескольких conds
для одного события, предпочтительнее избегать cond
и вместо этого использовать несколько типов событий.
Защитная функция in
¶
Свойство in
принимает идентификатор состояния в качестве аргумента и возвращает true
тогда и только тогда, когда этот узел состояния активен в текущем состоянии. Например, мы можем добавить защитную функцию к светофору:
const lightMachine = createMachine({
id: 'light',
initial: 'green',
states: {
green: {
on: {
TIMER: { target: 'yellow' },
},
},
yellow: {
on: {
TIMER: { target: 'red' },
},
},
red: {
initial: 'walk',
states: {
walk: {
/* ... */
},
wait: {
/* ... */
},
stop: {
/* ... */
},
},
on: {
TIMER: [
{
target: 'green',
in: '#light.red.stop',
},
],
},
},
},
});
Когда защитная функция in
присутствует с другими защитными функциями cond
в том же переходе, все защитные функции должны вернуть true
, чтобы переход был выполнен.
Подсказка
Использование защитных функций in
обычно является признаком того, что автомат можно реорганизовать таким образом, чтобы в их использовании не было необходимости. По возможности избегайте защитных функций in
.