Переходы¶
Переходы (Transitions) определяют, как конечный автомат реагирует на события.
API¶
Переходы состояний определяются на узлах состояний в свойстве on
:
import { createMachine } from 'xstate';
const promiseMachine = createMachine({
id: 'promise',
initial: 'pending',
states: {
pending: {
on: {
// state transition (shorthand)
// this is equivalent to { target: 'resolved' }
RESOLVE: 'resolved',
// state transition (object)
REJECT: {
target: 'rejected',
},
},
},
resolved: {
type: 'final',
},
rejected: {
type: 'final',
},
},
});
const { initialState } = promiseMachine;
console.log(initialState.value);
// => 'pending'
const nextState = promiseMachine.transition(initialState, {
type: 'RESOLVE',
});
console.log(nextState.value);
// => 'resolved'
В приведенном выше примере, когда автомат находится в состоянии pending
и получает событие RESOLVE
, он переходит в состояние resolved
.
Переход между состояниями можно определить как:
- строка, например
RESOLVE: 'resolved'
, что эквивалентно ... - объект со свойством
target
, например,RESOLVE: {target: 'resolved'}
, - массив объектов перехода, которые используются для условных переходов
Метод автомата .transition()
¶
Метод machine.transition(...)
— это чистая функция, которая принимает два аргумента:
Метов возвращает новый экземпляр State
, который является результатом выполнения всех переходов, разрешенных текущим состоянием и событием.
const lightMachine = createMachine({
/* ... */
});
const greenState = lightMachine.initialState;
// determine next state based on current state and event
const yellowState = lightMachine.transition(greenState, {
type: 'TIMER',
});
console.log(yellowState.value);
// => 'yellow'
Выбор разрешенных переходов¶
Разрешенный переход (enabled transition) — это переход, который будет выполняться условно, в зависимости от текущего состояния и события. Он будет принят тогда и только тогда, когда:
- он определяется на узле состояния, который соответствует текущему значению состояния
- защитник перехода (свойство
cond
) вернулtrue
- он не заменяется более специфичным переходом.
В иерархических автоматах переходы имеют приоритет в зависимости от того, насколько глубоко они находятся в дереве; более глубокие переходы более конкретны и, следовательно, имеют более высокий приоритет. Это работает аналогично тому, как работают события DOM: если вы нажмете кнопку, обработчик события щелчка непосредственно на кнопке будет более конкретным, чем обработчик события щелчка в окне.
const wizardMachine = createMachine({
id: 'wizard',
initial: 'open',
states: {
open: {
initial: 'step1',
states: {
step1: {
on: {
NEXT: { target: 'step2' },
},
},
step2: {
/* ... */
},
step3: {
/* ... */
},
},
on: {
NEXT: { target: 'goodbye' },
CLOSE: { target: 'closed' },
},
},
goodbye: {
on: {
CLOSE: { target: 'closed' },
},
},
closed: {
type: 'final',
},
},
});
// { open: 'step1' }
const { initialState } = wizardMachine;
// the NEXT transition defined on 'open.step1'
// supersedes the NEXT transition defined
// on the parent 'open' state
const nextStepState = wizardMachine.transition(
initialState,
{ type: 'NEXT' }
);
console.log(nextStepState.value);
// => { open: 'step2' }
// there is no CLOSE transition on 'open.step1'
// so the event is passed up to the parent
// 'open' state, where it is defined
const closedState = wizardMachine.transition(initialState, {
type: 'CLOSE',
});
console.log(closedState.value);
// => 'closed'
Дескрипторы событий¶
Дескриптор события (Event Descriptors) — это строка, описывающая тип события, которому будет соответствовать переход. Часто это эквивалентно свойству event.type
объекта event
, отправленного на конечный автомат:
// ...
{
on: {
// "CLICK" is the event descriptor.
// This transition matches events with { type: 'CLICK' }
CLICK: 'someState',
// "SUBMIT" is the event descriptor.
// This transition matches events with { type: 'SUBMIT' }
SUBMIT: 'anotherState'
}
}
// ...
Другие дескрипторы событий включают:
- Дескрипторы нулевых событий
""
(Null event descriptors), которые не соответствуют никаким событиям (т. е. "нулевые" события) и представляют собой переходы, выполненные сразу после входа в состояние. - Дескрипторы событий по-умолчанию
"*"
(Wildcard event descriptors) (для версии 4.7+), которые срабатывают, если никакие другие события не подошли.
Переходы без смены состояния¶
Переходы без смены состояния (Self Transitions) — это когда состояние переходит в само себя, из которого оно может выйти, а затем снова войти в себя. Они могут быть внутренними или внешними:
- Внутренний переход (internal transition) не будет ни выходом, ни повторным входом, но может входить в другие дочерние состояния.
- Внешний переход (external transition) выйдет и повторно войдет в себя, а также может выйти или войти в дочерние состояния.
По умолчанию все переходы с указанной целью являются внешними.
См. действия при переходах без смены состояния для детальной информации, как это происходит.
Внутренние переходы¶
Внутренний переход (internal transition) — это переход, который не выходит из своего узла состояния. Внутренние переходы создаются путем указания относительной цели (например, '.left'
) или путем явной установки {internal: true}
перехода. Например, рассмотрим автомат, который устанавливает абзац текста для выравнивания 'left'
, 'right'
, 'center'
или 'justify'
:
import { createMachine } from 'xstate';
const wordMachine = createMachine({
id: 'word',
initial: 'left',
states: {
left: {},
right: {},
center: {},
justify: {},
},
on: {
// внутренние переходы
LEFT_CLICK: '.left',
RIGHT_CLICK: { target: '.right' }, // идентично '.right'
CENTER_CLICK: { target: '.center', internal: true }, // идентично '.center'
JUSTIFY_CLICK: { target: '.justify', internal: true }, // идентично '.justify'
},
});
Вышеупомянутый автомат запустится в состоянии left
и в зависимости от того, что будет нажато, внутренне перейдет в другие дочерние состояния. Кроме того, поскольку переходы являются внутренними, вход, выход или какие-либо действия, определенные в родительском узле состояния, заново не выполняются.
Переходы, у которых есть {target: undefined}
(или нет target
), также являются внутренними переходами:
const buttonMachine = createMachine({
id: 'button',
initial: 'inactive',
states: {
inactive: {
on: { PUSH: 'active' },
},
active: {
on: {
// No target - internal transition
PUSH: {
actions: 'logPushed',
},
},
},
},
});
Шпаргалка по внутренним переходам:
EVENT: '.foo'
— внутренний переход к дочернему состояниюEVENT: { target: '.foo' }
— внутренний переход к дочернему состоянию (начинается с'.'
)EVENT: undefined
— запрещенный переходEVENT: { actions: [ ... ] }
— внутренний переход без смены состоянияEVENT: { actions: [ ... ], internal: true }
— внутренний переход без смены состояния, идентичен предыдущемуEVENT: { target: undefined, actions: [ ... ] }
— внутренний переход без смены состояния, идентичен предыдущему
Внешние переходы¶
Внешние переходы (external transition) будут выходить и повторно входить в узел состояния, в котором определен переход. В приведенном выше примере для родительского узла состояния word
(корневого узла состояния) при переходах выполняются действия выхода и входа.
По умолчанию переходы являются внешними, но любой переход можно сделать явно внешним, установив для перехода {internal: false}
.
// ...
on: {
// external transitions
LEFT_CLICK: 'word.left',
RIGHT_CLICK: 'word.right',
CENTER_CLICK: {
target: '.center', internal: false }, // идентично 'word.center'
JUSTIFY_CLICK: {
target: 'word.justify', internal: false } // идентично 'word.justify'
}
// ...
Каждый переход, описанный выше, является внешним, и для него будут выполняться действия выхода и входа родительского состояния.
Шпаргалка по внешним переходам:
EVENT: { target: 'foo' }
— все переходы в соседние узлы состояния — внешниеEVENT: { target: '#someTarget' }
— все переходы к другим узлам состояния — внешниеEVENT: { target: 'same.foo' }
— внешний переход к собственному дочернему узлу состояния (эквивалентно{ target: '.foo', internal: false }
)EVENT: { target: '.foo', internal: false }
— внешний переход к дочернему узлу состояния — в противном случае это был бы внутренний переходEVENT: { actions: [ ... ], internal: false }
— внешний переход без смены состоянияEVENT: { target: undefined, actions: [ ... ], internal: false }
— внешний переход без смены состояния, аналогичен предыдущему
Проходные переходы¶
Warning
Синтаксис пустой строки ({on: {'': ...}}
) не рекомендуется использовать c версии 5. Следует отдавать предпочтение новому синтаксису always
c версии 4.11+. См. ниже раздел о переходах без событий, которые аналогичны проходным переходам.
Проходной переход (transient transition) — это переход, который активируется нулевым событием (null event). Другими словами, это переход, который выполняется немедленно (т. е. без инициирующего события), пока выполняются какие-либо условия:
const gameMachine = createMachine(
{
id: 'game',
initial: 'playing',
context: {
points: 0,
},
states: {
playing: {
on: {
// Transient transition
// Will transition to either 'win' or 'lose' immediately upon
// (re)entering 'playing' state if the condition is met.
'': [
{ target: 'win', cond: 'didPlayerWin' },
{ target: 'lose', cond: 'didPlayerLose' },
],
// Self-transition
AWARD_POINTS: {
actions: assign({
points: 100,
}),
},
},
},
win: { type: 'final' },
lose: { type: 'final' },
},
},
{
guards: {
didPlayerWin: (context, event) => {
// check if player won
return context.points > 99;
},
didPlayerLose: (context, event) => {
// check if player lost
return context.points < 0;
},
},
}
);
const gameService = interpret(gameMachine)
.onTransition((state) => console.log(state.value))
.start();
// Still in 'playing' state because no conditions of
// transient transition were met
// => 'playing'
// When 'AWARD_POINTS' is sent, a self-transition to 'PLAYING' occurs.
// The transient transition to 'win' is taken because the 'didPlayerWin'
// condition is satisfied.
gameService.send('AWARD_POINTS');
// => 'win'
Как и переходы, проходные переходы могут быть указаны как один переход (например, '': 'someTarget'
) или как массив условных переходов. Если никакие условные переходы при проходном переходе не выполняются, автомат остается в том же состоянии.
Нулевые события всегда «отправляются» для каждого перехода, внутреннего или внешнего.
Безсобытийные "Always" переходы¶
Начиная с версии 4.11+
Бессобытийный переход (Eventless transition) — это переход, который всегда выполняется, когда автомат находится в состоянии, в котором он определен, и когда его cond
защитной функцией оценивается как true
. Они проверяются:
- сразу при входе в узел состояния
- каждый раз, когда машина получает действующее событие (независимо от того, запускает ли событие внутренний или внешний переход)
Бессобытийные переходы определены в свойстве always
узла состояния:
const gameMachine = createMachine(
{
id: 'game',
initial: 'playing',
context: {
points: 0,
},
states: {
playing: {
// Eventless transition
// Will transition to either 'win' or 'lose' immediately upon
// entering 'playing' state or receiving AWARD_POINTS event
// if the condition is met.
always: [
{ target: 'win', cond: 'didPlayerWin' },
{ target: 'lose', cond: 'didPlayerLose' },
],
on: {
// Self-transition
AWARD_POINTS: {
actions: assign({
points: 100,
}),
},
},
},
win: { type: 'final' },
lose: { type: 'final' },
},
},
{
guards: {
didPlayerWin: (context, event) => {
// check if player won
return context.points > 99;
},
didPlayerLose: (context, event) => {
// check if player lost
return context.points < 0;
},
},
}
);
const gameService = interpret(gameMachine)
.onTransition((state) => console.log(state.value))
.start();
// Still in 'playing' state because no conditions of
// transient transition were met
// => 'playing'
// When 'AWARD_POINTS' is sent, a self-transition to 'PLAYING' occurs.
// The transient transition to 'win' is taken because the 'didPlayerWin'
// condition is satisfied.
gameService.send({ type: 'AWARD_POINTS' });
// => 'win'
Бессобытийные переходы против переходов по-умолчанию¶
- Переходы по-умолчанию (Wildcard transitions) не проверяются при входе в узлы состояния, а бессобытийные переходы - проверяются. Защитные функции для переходов без событий выполняются перед тем, как делать что-либо еще (даже до выполнения защитных функций входных действий).
- Повторная оценка бессобытийных переходов запускается любым действующим событием. Повторная оценка переходов по-умолчанию запускается только событием, не совпадающим с явными дескрипторами событий.
Внимание
При неправильном использовании бессобытийных переходов можно создавать бесконечные циклы.
Бессобытийные переходы следует определять с помощью target
, cond
+ target
, cond
+ actions
или cond
+ target
+ actions
. Цель, если она объявлена, должна отличаться от узла текущего состояния. Бессобытийные переходы без target
или cond
вызовут бесконечный цикл. Переходы с cond
и actions
могут превратиться в бесконечный цикл, если его защитная функция cond
продолжает возвращать true
.
Подсказка
Когда проверяются бессобытийные переходы, их защитные функции повторно запускаются до тех пор, пока все они не вернут false
, или переход с target
не будет подтвержден. Каждый раз, когда какая-либо защитная функция возвращает true
во время этого процесса, связанные с ним действия будут выполнены один раз. Таким образом, возможно, что во время одной микрозадачи некоторые переходы без целей выполняются несколько раз.
Это контрастирует с обычными переходами, где всегда можно сделать максимум один переход.
Запрещенные переходы¶
В XState «запрещенный» переход ("forbidden" transition) — это переход, который указывает, что переход состояния не должен происходить с указанным событием. То есть при запрещенном переходе ничего не должно происходить, и событие не должно обрабатываться узлами родительского состояния.
Запрещенный переход задается путем явного указания target
как undefined
. Это то же самое, что указать его как внутренний переход без действий:
on: {
// запрещенный переход
LOG: undefined,
// идентично
LOG: {
actions: []
}
}
Например, мы можем смоделировать, что телеметрия может регистрироваться для всех событий, кроме случаев, когда пользователь вводит личную информацию:
const formMachine = createMachine({
id: 'form',
initial: 'firstPage',
states: {
firstPage: {
/* ... */
},
secondPage: {
/* ... */
},
userInfoPage: {
on: {
// явно запретить событию LOG что-либо делать
// или любые переходы в любое другое состояние
LOG: undefined,
},
},
},
on: {
LOG: {
actions: 'logTelemetry',
},
},
});
Подсказка
Обратите внимание, что при определении нескольких переходов с одним и тем же именем события в иерархической цепочке «предок-потомок» будет использоваться только самый внутренний переход. В приведенном выше примере именно поэтому действие logTelemetry
, определенное в родительском событии LOG
, не будет выполняться, как только компьютер достигнет состояния userInfoPage
.
Несколько целей¶
Переход, основанный на одном событии, может иметь несколько целевых узлов состояния. Это необычно и допустимо только в том случае, если узлы состояния легальны; например, переход к двум узлам состояния одного и того же уровня в узле составного состояния является недопустимым, поскольку (непараллельный) конечный автомат может находиться только в одном состоянии в любой момент времени.
Несколько целей указываются в виде массива в target: [...]
, где каждая цель в массиве является относительным ключом или идентификатором узла состояния, как и отдельные цели.
const settingsMachine = createMachine({
id: 'settings',
type: 'parallel',
states: {
mode: {
initial: 'active',
states: {
inactive: {},
pending: {},
active: {},
},
},
status: {
initial: 'enabled',
states: {
disabled: {},
enabled: {},
},
},
},
on: {
// Multiple targets
DEACTIVATE: {
target: ['.mode.inactive', '.status.disabled'],
},
},
});
События по-умолчанию¶
Начиная с версии 4.7+
Переход, указанный с помощью дескриптора события по-умолчанию «*
» (wildcard event descriptor), активируется любым событием. Это означает, что любое событие будет соответствовать переходу, который имеет: {"*": ...}
, и если защитные функции вернут true
, этот переход будет выполнен.
Явные дескрипторы событий всегда будут выбираться вместо дескрипторов событий по-умолчанию, если переходы не определены в массиве. В этом случае порядок переходов в массиве и определяет, какой из них будет выбран.
// Для события SOME_EVENT будет выбран переход "here"
on: {
"*": "elsewhere",
"SOME_EVENT": "here"
}
// Для события SOME_EVENT будет выбран переход по-умолчанию "elsewhere"
on: [
{ event: "*", target: "elsewhere" },
{ event: "SOME_EVENT", target: "here" },
]
Подсказка
Дескрипторы по-умолчанию (Wildcard descriptors) не ведут себя так же, как проходные переходы (transient transitions) (с нулевыми (null) дескрипторами событий). В то время как проходные переходы будут выполняться немедленно, когда состояние активно, переходы по-умолчанию (wildcard transitions) по-прежнему нуждаются в каком-либо событии, которое должно быть отправлено в его состояние для запуска.
Пример:
const quietMachine = createMachine({
id: 'quiet',
initial: 'idle',
states: {
idle: {
on: {
WHISPER: undefined,
// On any event besides a WHISPER, transition to the 'disturbed' state
'*': 'disturbed',
},
},
disturbed: {},
},
});
quietMachine.transition(quietMachine.initialState, {
type: 'WHISPER',
});
// => State { value: 'idle' }
quietMachine.transition(quietMachine.initialState, {
type: 'SOME_EVENT',
});
// => State { value: 'disturbed' }
Вопросы и ответы¶
Как мне выполнить логику if / else при переходах?¶
Иногда вам захочется сказать:
- Если что-то
true
, перейти в это состояние - Если что-то еще
true
, перейдите в это состояние - Иначе перейти в это состояние
Для этого можно использовать защищенные переходы.
Как мне перейти в любое состояние?¶
Вы можете перейти в любое состояние, присвоив этому состоянию собственный идентификатор и используя target: '#customId'
. Вы можете прочитать полную документацию по пользовательским идентификаторам здесь.
Это позволяет вам переходить от дочерних состояний к одноуровневым родительским состояниям, например, в событиях CANCEL
и done
в этом примере: