I'm practicing modeling ui behavior with xstate, and unfortunately got stuck at the beginning with this seemingly simple app (see screencap here).
Here's my first attempt to model it but I can't figure out how to model the 15s timer thing on the chart, nor did I know where the timer logic should go in the code (I presume it belongs to actions?)
Here's the machine code I got so far, would appreciate any help to get this through :)
const BusMachine = Machine(
{
id: "Bus App",
context: {
data: 1,
reloadCounter: 15,
},
initial: "display",
states: {
display: {
onEntry: 'loadData',
// onExit: ['bar'],
on: {
"RELOAD": "loading"
}
},
loading: {
on: {
"LOAD_FAIL": "error",
"LOAD_OK": "display"
}
},
error: {
on: {
"RELOAD": "loading"
}
}
},
on: {
//
}
},
{
actions: {
loadData: assign((ctx, evt) => {
console.log( '\n[action loadData]', ctx, evt )
const newData = getData()
return {...ctx, data: newData}
// countDownId = setInterval(this.countDown, 1000)
// this.countDown();
}),
bar: assign((ctx, evt) => {
console.log( '[action bar]', ctx, evt )
return ctx
}),
}
},
)
@carloslfu wrote an excellent article that walks through how he solved a very similar problem:
https://medium.com/@carloslfu/modeling-a-screensaver-with-a-statechart-a-real-use-case-f57301682570
The visible countdown timer ticking on every second makes me wonder if a callback service might be a good fit.
You could have that service call back every second with a countdown event to perform an internal transition and assign the new counter value, and then call back with an event to transition to the loading state when the countdown expires.
You would want to make sure your countdown service returns a cleanup function, so that your countdown is cancelled when transitioning away to the loading state (ex. via the pull to refresh while in the display state).
I'd also be inclined to start in the loading state initially, loading the data with a Promise service there, rather than trying to combine that initial load into the display state.
BTW - be sure to check out the gitter chat and spectrum chat. It's a great place to pose these kinds of questions and get input from more people. David has attracted a great community there.
Thanks @johnyanarella for all the tips, now flipping through the doc and reworking the code hoping to make it work, will report back final result.
Alright after reading through all the docs here's what I've got so far, __still got stuck by what to do once the 15s timer is up__ (i.e. don't know how to transit to the loading state to trigger the promise call there), see code below.
My guts feeling is I'm doing it wrong (or at least not following the best possible xstate way), would appreciate any help, thanks!
const BusMachine = Machine(
{
id: "Bus App",
context: {
data: null,
timer: len,
errMsg: null,
},
initial: "idle",
states: {
idle: {
invoke: {
id: 'getDataByInterval',
src: (ctx, event) => (callback, onEvent) => {
countdownId = setInterval(() => {
callback('COUNTDOWN')
}, 1000);
return () => clearInterval(countdownId);
}
},
on: {
COUNTDOWN: {
// target: "loading",
// cond: (ctx, evt) => (ctx.timer !== 0),
actions: assign((ctx, e) => {
const t = ctx.timer - 1
if( t < 0 ) {
// [QUESITON]
// how to trigger a state transition within an action when the timer is up?
// tried both `send` and `sendParent` to no avail
sendParent('DEBUG')
}
return {...ctx, timer: t < 0 ? len : t }
})
},
},
},
loading: {
invoke: {
id: 'getSomeData',
src: (ctx, e) => {
return getData()
},
onDone: {
target: 'idle',
actions: assign((ctx, evt) => {
return {...ctx, data: evt.data}
})
},
onError: {
target: 'error',
actions: assign((ctx, evt) =>{
return {...ctx, errorMsg: evt}
})
}
}
},
error: {
on: {
"RELOAD": "loading"
}
}
},
on: {
"DEBUG": {
target: 'loading',
actions: () => console.log( 'DEBUG run', )
}
}
},
)
const getData = () => {
return new Promise((resolve, reject) => {
const d = (Math.random(100) * 100).toFixed(1)
setTimeout(() => resolve(d), 2000)
// setTimeout(() => reject('network error'), 2000)
})
}
Very close!
As you get used to the statechart terminology, try to think about actions as "fire and forget" side-effects, rather than a thing that acts on the machine or transitions you to another state (with the exception of the special send() action, which is used relatively rarely - usually stimuli arrive externally or from a service).
assign(), in particular, is just concerned with computing and writing values into the context based on the previous context and event. BTW - You also don't have to worry about spreading the old context in, when updating it. Pretty convenient!
In this case, it's the countdown service that should send the event that transitions you to the "loading" state.
Try this out in the XState visualizer - be sure to check out the content of the "State" tab as it runs:
const duration = 15
const BusMachine = Machine(
{
id: 'Bus App',
context: {
data: null,
counter: null,
errMsg: null,
},
initial: 'loading',
states: {
idle: {
invoke: {
id: 'countdown',
src: 'countdown'
},
on: {
// internal transition, countdown service continues on unaffected
COUNTDOWN: {
actions: 'updateCounter'
},
// external transition, countdown service will be stopped
// could be triggered by pull to refresh, or countdown service
RELOAD: {
target: 'loading'
}
},
},
loading: {
onEntry: 'clearCounter',
invoke: {
id: 'getData',
src: 'getData',
onDone: {
target: 'idle',
actions: 'updateData'
},
onError: {
target: 'error',
actions: 'updateErrMsg'
}
}
},
error: {
on: {
RELOAD: 'loading'
}
}
}
},
{
actions: {
clearCounter: assign({
counter: null
}),
updateCounter: assign({
counter: (ctx, evt) => evt.counter
}),
updateData: assign({
data: (ctx, evt) => evt.data
}),
updateErrMsg: assign({
errMsg: (ctx, evt) => evt.data.message
})
},
services: {
countdown: (ctx, evt) => callback => {
let counter = duration
countdownId = setInterval(() => {
if (counter === 0) {
callback('RELOAD')
}
else {
callback({ type: 'COUNTDOWN', counter })
}
counter--
}, 1000);
return () => clearInterval(countdownId)
},
getData: () => {
return new Promise((resolve, reject) => {
const d = (Math.random(100) * 100).toFixed(1)
setTimeout(() => resolve(d), 2000)
// setTimeout(() => reject(new Error('network error')), 2000)
})
}
}
}
)
For clarity, I moved the action and service implementations out of the configuration, and into the options.
Early in the design of your state charts, it can be helpful to omit these implementations, as you puzzle through the behavior of your machine in something closer to plain english. It can also be helpful as the size of your statechart grows, because it enables you to see the workflow separate from the finer details, and focus on implementing those actions later.
That's very detailed and timely answers, thanks you very much @johnyanarella, very much appreciate it!
I've got the prototype running here, with some minor features added (aims to verify if subtle ui details can be supported by xstate), please feel free to point out any mistakes and suggestions!
unhappy path could be verifiedloading... message letting users know what's going on in the backgroundreload button to refresh immediatelyOut of curiosity, building frontend apps using this fsm-driven approach seemed to get a lot of things right, and xstate plus react goes really well together, I'm wondering why this approach hasn't been wildly adopted by the public? Is it simply due to lack of publicity, or there's indeed technical barriers to prevent people from doing so?
I'm wondering why this approach hasn't been wildly adopted by the public?
That's a great question, and I should really write a blog/documentation entry on this.
First, state machines and statecharts _are_ widely adopted in other areas of technology such as embedded systems, hardware, aeronautics and automotive tech, game development, and more.
There is definitely a learning curve to using state machines and statecharts; you can't simply start coding away, you have to model your application first. This takes time and experience, but at least with tools like XState, you can modify your model as you go. Front-end developers in large are not used to modeling code up-front, so it's a change in discipline/behavior, and a departure from the status-quo "move fast break things" mentality.
However, I (and others, like @mogsie) are passionate about this tried-and-true methodology for modeling applications and realize that there are many more benefits than just "a different way of managing state", such as:
And this project only really started taking off less than 2 years ago, so it's pretty young! There's still time for the community to grow 🚀
So sounds like this approach requires a mental model change just like from “jquery everywhere” to “thinking in component/react” that happened a couple of years ago, which is totally understandable.
I’m indeed very excited about all the points you mentioned above, even if just a few of them really happened it would still be a huge thing for the frontend community, this approached looks to me just like another paradigm shift coming, if not the biggest revolution after react brought component-based/declarative style approach to the world and changed everything a few years ago.
Very looking forward to the bright future of xstate and thanks for putting in the efforts to bring great things to developers, you guys rock!
Agreed. I've only been using XState for a few weeks, but I haven't been this excited since I first encountered React.
It definitely requires a "give it five minutes" kind of mental model shift (like React and JSX did back then). But afterward nothing looks the same, and you have a new vocabulary to reason about and describe problems more precisely.
I personally think David's library is poised to take off this year (hockey stick growth), given the current pain points in the React world:
bag-of-state + action => bag-of-state model vs the discrete-state + event => discrete-state, actions model;useReducer() is too tight a straight-jacket, and be wary of post-async-operation getState() hacks as they run afoul of the next item); The React team is doing amazing work, but their bold vision involves some waiting yet and a lot of near term uncertainty.
XState provides an answer for those problems today. Being UI library agnostic, XState also makes for an excellent insurance policy for your most important logic, should you need to move elsewhere in the future.
And that's before you even consider all the amazing things that an executable declarative statechart makes possible...
Predictive analytics (based on above) -> reinforcement learning for adaptive UIs
Do you mean since we have statistical data on usage for each state, we might be able to predict user's next move (i.e what's the most possible state that a user will be moving into), and preload data for that state or even preload the view so that visual transition will be much smoother hence providing a better user experience?
Most helpful comment
That's a great question, and I should really write a blog/documentation entry on this.
First, state machines and statecharts _are_ widely adopted in other areas of technology such as embedded systems, hardware, aeronautics and automotive tech, game development, and more.
There is definitely a learning curve to using state machines and statecharts; you can't simply start coding away, you have to model your application first. This takes time and experience, but at least with tools like XState, you can modify your model as you go. Front-end developers in large are not used to modeling code up-front, so it's a change in discipline/behavior, and a departure from the status-quo "move fast break things" mentality.
However, I (and others, like @mogsie) are passionate about this tried-and-true methodology for modeling applications and realize that there are many more benefits than just "a different way of managing state", such as:
And this project only really started taking off less than 2 years ago, so it's pretty young! There's still time for the community to grow 🚀