Why If we need to add some feature it is intuitive to introduce some a boolean flag "Boolean" programming is bad Processes in the app better to be based on named states As a programmer we have to code the transitions between states xState helps with this XState XState is a state management and orchestration solution for JavaScript apps XState allows to manage workflow state by creating a model logic as actors and state machines Can be used in the frontend, backend, or wherever JavaScript runs npm i xstate @xstate/react install with npm
import { createMachine, assign, createActor } from 'xstate';
const countMachine = createMachine({
context: {
count: 0,
},
on: {
INC: {
actions: assign({
count: ({ context }) => context.count + 1,
}),
},
DEC: {
actions: assign({
count: ({ context }) => context.count - 1,
}),
},
SET: {
actions: assign({
count: ({ event }) => event.value,
}),
},
},
});
const countActor = createActor(countMachine).start();
countActor.subscribe((state) => {
console.log(state.context.count);
});
countActor.send({ type: 'INC' });
// logs 1
countActor.send({ type: 'DEC' });
// logs 0
countActor.send({ type: 'SET', value: 10 });
// logs 10
const textMachine = createMachine({
context: {
committedValue: '',
value: ''
},
initial: 'reading',
states: {
reading: {
on: {
'text.edit': { target: 'editing' }
}
},
editing: {
on: {
'text.change': {
actions: assign({
value: ({ event }) => event.value
})
},
'text.commit': {
actions: assign({
committedValue: ({ context }) => context.value
}),
target: 'reading'
},
'text.cancel': {
actions: assign({
value: ({ context }) => context.committedValue
}),
target: 'reading'
}
}
}
}
})
const textActor = createActor(textMachine).start()
textActor.subscribe((state) => {
console.log(state.context.value)
})
textActor.send({ type: 'text.edit' }) // logs ''
textActor.send({ type: 'text.change', value: 'Hello' }) // logs 'Hello'
textActor.send({ type: 'text.commit' }) // logs 'Hello'
textActor.send({ type: 'text.edit' }) // logs 'Hello'
textActor.send({ type: 'text.change', value: 'Hello world' }) // logs 'Hello world'
textActor.send({ type: 'text.cancel' }) // logs 'Hello'
Actor Actor is a mathematical model for building message-based systems by using actors to communicate When you run a state machine in XState, it becomes an actor Actors can communicate with each other via asynchronous message passing - events Actors process one message at a time Actor has its own internal state that can only be updated by the actor itself Actor may update its internal state in response to a message it receives Actors can create new actors Actors can be created and destroyed as needed to handle the workload efficiently
import { createMachine, createActor } from 'xstate';
const toggleMachine = createMachine({
id: 'toggle',
initial: 'Inactive',
states: {
Inactive: {
on: { toggle: 'Active' },
},
Active: {
on: { toggle: 'Inactive' },
},
},
});
// Create an actor that you can send events to.
// Note: the actor is not started yet!
const actor = createActor(toggleMachine);
// Subscribe to snapshots (emitted state changes) from the actor
actor.subscribe((snapshot) => {
console.log('Value:', snapshot.value);
});
// Start the actor
actor.start(); // logs 'Inactive'
// Send events
actor.send({ type: 'toggle' }); // logs 'Active'
actor.send({ type: 'toggle' }); // logs 'Inactive'
Main principles https://stately.ai/docs/state-machines-and-statecharts A state machine is used to describe the behavior of something The machine describes the thing's states and the transitions between those states A state describes the machine's status or mode, which could be as simple as Asleep and Awake A state machine can only be in one state at a time When a state machine starts, it enters the initial state first. Events cause transitions When an event happens, the machine transitions to the next state The dog goes between asleep and awake through the wake up and fall asleep events.
import { createMachine } from 'xstate';
const dogMachine = createMachine({
id: 'dog',
initial: 'asleep',
states: {
asleep: {
on: {
'wakes up': 'awake',
}
},
awake: {
on: {
'falls asleep': 'asleep',
}
},
//...
}
});
It's a Finite State Machine (FSM) because it has a finite number of states Events and transitions are defined inside the on property of a state. Transition has a source state which comes before the transition, and a target state, which comes after the transition Most processes with states will have a final state, the last state when the process is finished Parent state A parent state is a state that can contain more states, also known as child states. Child states can only happen when the parent state is happening An atomic state is a state that doesn't have any child states.
export const walkMachine = createMachine({
id: 'walk',
initial: 'waiting',
states: {
waiting: {
on: {
'leave home': {
target: 'on a walk',
reenter: true,
},
},
},
'on a walk': {
initial: 'walking',
on: {
'get back home': {
target: 'walk ended',
reenter: true,
},
},
states: {
walking: {
on: {
'speed up': 'running',
},
},
running: {
on: {
'slow down': 'walking',
},
},
},
},
'walk ended': {
type: 'final',
},
},
})
Parallel state A parallel state is a state where all of its child states, also known as regions , are active simultaneously
export const callMachine = createMachine({
id: 'call machine',
type: 'parallel',
states: {
mic: {
initial: 'muted',
states: {
muted: {
on: {
unmute: 'unmuted',
},
},
unmuted: {
on: {
mute: 'muted',
},
},
},
},
video: {
initial: 'showing video',
states: {
'showing video': {
on: {
hide: 'hiding video',
},
},
'hiding video': {
on: {
show: 'showing video',
},
},
},
},
},
})
Self-transition A self-transition is when an event happens, but the transition returns to the same state Useful for changing context and/or executing actions without changing the finite state
import { createMachine, assign } from 'xstate';
const machine = createMachine({
context: { count: 0 },
on: {
someEvent: {
// No target
actions: assign({
count: ({context}) => context.count + 1,
})
}
}
});
Context data Context is how you store data in a state machine actor
import { assign, createMachine } from 'xstate';
export const toggleMachine = createMachine({
id: 'toggle',
context: { count: 0 },
initial: 'Inactive',
states: {
Inactive: {
on: { toggle: 'Active' },
},
Active: {
entry: assign({
count: ({ context }) => context.count + 1
}),
on: { toggle: 'Inactive' },
after: { 2000: 'Inactive' },
},
},
});
Input & Guards Input is how initial data can be provided to a machine actor Guards are used to conditionally allow or disallow transitions
import { assign, createMachine } from 'xstate';
export const toggleMachine = createMachine({
id: 'toggle',
context: ({ input }) => ({
count: 0,
maxCount: input.maxCount
}),
initial: 'Inactive',
states: {
Inactive: {
on: {
toggle: {
// Only trigger toggle transition if count is less than maxCount
guard: ({ context }) => context.count < context.maxCount,
target: 'Active'
}
}
},
Active: {
entry: assign({
count: ({ context }) => context.count + 1
}),
on: { toggle: 'Inactive' },
after: { 2000: 'Inactive' },
},
},
});
const actor = createActor(toggleMachine, {
input: { maxCount: 10 }
});
actor.subscribe(snapshot => {
console.log('State:', snapshot.value);
});
actor.start();
actor.send({ type: 'toggle' });
Machine with React
import { assign, createMachine } from 'xstate';
import { useMachine } from '@xstate/react';
export const toggleMachine = createMachine({
id: 'toggle',
context: { count: 0 },
initial: 'Inactive',
states: {
Inactive: {
on: { toggle: 'Active' },
},
Active: {
entry: assign({
count: ({ context }) => context.count + 1
}),
on: { toggle: 'Inactive' },
after: { 2000: 'Inactive' },
},
},
});
const App = () => {
const [state, send] = useMachine(toggleMachine);
return (
<div>
<div>Value: {state.value}</div>
<button onClick={() => send({ type: 'toggle' })}>
Toggle
</button>
</div>
);
};