XState Version 4 Released š
TL;DR: https://github.com/davidkpiano/xstate
Itās been over a year since I first talked about state machines and statecharts to the web community at React Rally 2017. To be honest, I had no idea how it would be received. Deterministic finite automata isnāt exactly the most exciting topic, and most developers either arenāt familiar with them or have studied them in classes, and dismissed them as too theoretical or academic to apply to something as ever-evolving as modern UI development.
To my surprise, the ideas I presented from brilliant minds such as David Harel and Ian Horrocks actually clicked with many others, developers and designers alike! Thereās many reasons why Iām passionate about using statecharts in developing UIs:
- Visualizing application behavior and logic
- Natural prevention of āimpossible statesā
- Automatic test generation and running
- Full simulation in headless environments
- Reduced code complexity
- The SCXML specification (use statecharts in other languages!)
- Improved developer/designer handoff
- Accommodation of late-breaking changes and requirements
- Much, much less code
These ideas arenāt mine, and these ideas arenāt new (theyāre older than JavaScript itself). Theyāve been proven in other areas of tech, such as embedded electronics, automotive tech, avionics systems, and more. And with XState, weāre determined to bring them to the modern web.
XState version 4 is a major release with very few breaking changes, and many new features that are fully SCXML-compatible.
Assigning to context
Statecharts have a notion of āextended stateā, which is state that is more quantitative than qualitative; that is, itās not exactly finite. For example, you can describe a glass as either empty | filling | full
(finite state), or you can describe it by its actual volume, e.g., 123 ml
(extended state). In version 4, context
(i.e., extended state) can now be modeled declaratively inside the machine itself. With the assign
action, this gives you the ability to manipulate the extended state, just like Redux or other state management libraries:
const counterMachine = Machine({
id: 'counter',
context: { count: 0 },
initial: 'counting',
states: {
counting: {
on: {
INCREMENT: assign({ count: ctx => ctx.count + 1 }),
DECREMENT: assign({ count: ctx => ctx.count - 1 })
}
}
}
});
counterMachine
.transition(counterMachine.initialState, 'INCREMENT')
.context;
// => { count: 1 }
The transition
function is still pure, and the next State instance will provide the next value and context. So what's the difference between XState and the many other state management libraries?
Time travel. In the future.
This simple tic-tac-toe game is modeled as a statechart with XState. Using the exact same code, weāre able to write assertions and automatically generate tests for advanced use-cases like āit should be possible for the game to end in a draw.ā
In the future, there can be even more powerful predictive tools for testing and finding edge cases, since we can deterministically know how and when any value can change as it propagates through states and actions, instead of assuming a value can be modified at any time.
Invoking other machines
Many people have asked about whether they should design one giant, complex statechart for their app or split it into smaller statecharts. Statecharts describe the behavior of a single system, and applications are typically modeled by many systems communicating with each other, in some orchestrated way (hopefully). Multiple smaller statecharts are preferred ā you should isolate behavior and rely on communication (message-passing) between statecharts instead.
This is where invoke
comes into play. In XState version 4, invoke
enables you to create systems of statecharts that communicate with each other, which is compatible with the SCXML implementation. This closely resembles the Actor model.
In fact, a Promise can be considered its own state machine:
const userMachine = Machine({
id: 'user',
initial: 'loading',
context: { userID: 42 },
states: {
loading: {
invoke: {
src: (ctx, event) =>
fetch(`/api/users/${ctx.userID}`)
.then(response => response.json()),
onDone: 'success',
onError: 'failure'
}
},
success: {},
failure: {}
}
});
Parents can send events to children via the send('SOME_EVENT', { to: 'childID' })
action, and children can send events to parents via the sendParent('SOME_EVENT')
action.
See the XState docs for more info.
Interpreter
XState now comes with a built-in (completely optional) interpreter for running your statecharts in any framework (or no framework at all), using an event-emitter-based syntax:
import { Machine } from 'xstate';
import { interpret } from 'xstate/lib/interpreter';
const myMachine = // ... your Machine()
const myService = interpret(myMachine)
.onTransition(state => console.log(state));
// Start your service!
myService.start();
// Send events to your service
myService.send('SOME_EVENT');
myService.send({ type: 'COMPLEX_EVENT', data: { id: 42 } });
// Stop and clean up your service
myService.stop();
Since XStateās machine.transition()
function is pure, you're free to create your own interpreter, or integrate this interpreter into your projects. For example, the use-machine
library by Carlos Galarza lets you interpret XState machines in React hooks!
See the XState docs for more info.
Timers and Delays
Time is the most neglected variable, yet it is extremely important to manage in reactive systems. XState version 4 treats time as a first-class citizen with two new features:
- Delayed transitions with
after: { ... }
- Delayed events with e.g.,
send(event, { delay: 1000 })
Modeling transitions that happen over time can now be done declaratively:
{
green: {
// after 2 seconds, go to 'yellow'
after: {
2000: 'yellow'
}
}
}
In an interpreter that supports delayed transitions, the setTimeout
call for this will be canceled if the 'green'
state is exited, so no need to worry about unexpected transitions or cleaning up timers. Sending and canceling delayed events can also be done:
{
on: {
SOME_EVENT: {
actions: send('showAlert', { id: 'alert1', delay: 1000 })
}
},
// If this state is exited before 1000ms,
// the 'showAlert' action will not be sent.
onExit: cancel('alert1')
}
See the XState docs for more info.
Final States
I neglected to talk about final states so far, but with statecharts, theyāre actually incredibly useful. Instead of making awkward transitions from child states to parent-level states, you can decouple the logic with an onDone
transition:
{
red: {
initial: 'walk',
states: {
walk: {
after: { 2000: 'wait' }
},
wait: {
after: { 1000: 'stop' }
},
stop: {
type: 'final'
}
},
// When the final 'red.stop' state is reached,
// transition to 'green'
onDone: 'green'
}
}
See the XState docs for more info.
Visualizer
With this release comes a new visualizer, with support for everything in XState version 4, including parallel/hierarchical states, history/final states, timers, guards, and more. This visualizer is built in HTML, SVG, and React, with an emphasis on developer experience:
- Copy-paste your JavaScript
Machine()
code directly into the editor. TheMachine
andXState
variables are available to you in this scope. - Send custom events to the machine, such as
{ type: 'EVENT', data: { id: 42 } }
. - Collapse and expand nested states for visual clarity.
- Modify and update your statechart live.
- Save your statechart definitions (coming š ā for now, you can just save it as a gist).
- More to come!
Future Plans
This is just the beginning. Statecharts are a decades-old, well-researched way of modeling reactive systems, and there is so much potential in using software modeling principles in the highly dynamic, complex applications and user interfaces we work with every day. Hereās whatās planned for XState in the near future:
- Full SCXML conversion. This will allow statecharts defined in XState to be compatible with any SCXML interpreter, in any language, and uses a common standard for defining statecharts.
- More examples, in many different frameworks and libraries, such as Vue, React, Angular, RxJS, etc.
- Testing and analysis tools.
- Live debugging tools and browser extensions.
- Implementation of XState in other languages, such as ReasonML.
- Much, much more.
And finally, Iām planning on releasing a beta version of advanced statechart visualization, simulation, editing, and testing software in early 2019. Keep an eye out for that š
Special Thanks and the Community
The statecharts community has grown significantly over the last year, with so many members contributing incredible ideas, tools, and insights to improving the way we craft and model software on the web. Iām super thankful to:
- Erik Mogensen for his knowledge and shared passions about statecharts, including the invaluable resource statecharts.github.io
- Michele Bertoli for bringing
react-automata
to the React ecosystem, making it easier to model React apps as statecharts - Jon Bellah and Kyle Shevlin for creating lessons on XState (Learn State Machines and Egghead: State Transitions with XState)
- Luca Matteis for his wonderful insights on statecharts and especially behavioral programming, which is a related concept by David Harel, and well worth exploring
- Prof. David Harel of course, for inventing statecharts. He just released a new course called āProgramming for Everyoneā with Dr. Michal Gordon on statecharts and live sequence charts!
- Kate Compton for her inspiring experiments with finite state machines and more advanced uses like story-grammar generation with Tracery
And extra special thanks Tipe.io and the backers on the XState Open Collective for sponsoring the development of XState and making it possible for me to spend my limited free time working on this daily.
Letās make our code do more together. Thanks for reading! š