DEV Community

Cover image for No, disabling a button is not app logic.

No, disabling a button is not app logic.

David K. 🎹 on November 13, 2019

I'm going to start this post with an excerpt from the book "Constructing the User Interface with Statecharts", written by Ian Horrocks in 1999: U...
Collapse
 
felix profile image
Felix Guerin

Thank you so much for that article, it's a whole new way of looking at app logic for me and I really learned a lot!

I'm not exactly sure how I would replicate useEffect() outside of React and without using Xstate (or any other state machine library). Do you know of a framework/library agnostic way of doing this?

Collapse
 
adam_cyclones profile image
Adam Crockett 🌀

That's so funny, today I was to tinkering with xstate on its own and although the title is not talking about finite state machines I tapped to take a peek. I started seeing the switch and scrolled down to suggest xstate... Oh damn haha, anyway nice post 🥳

Collapse
 
adam_cyclones profile image
Adam Crockett 🌀

Hold on your the David who wrote xstate! I'm a big fan, trying to get Dyson to adopt this 🤞😤

Collapse
 
uriklar profile image
Uri Klar

Hi David, Thanks a lot for this write up.
One thing that wasn't very clear to me is the cancelling logic in the reducer example.

  • The cleanup function is inside the

    if (state.status === "loading")

    block. So how is it still being invoked when status changes to "idle"? (due to a cancel event)

  • How does the cleanup variable persist across renders?

In general i'd love a few words on cancelation logic since it doesn't look very trivial.
Thanks again!

Collapse
 
uriklar profile image
Uri Klar

Ok, so after debugging the sandbox a bit I think I get it...
The cleanup function (that turns canceled into true) only runs when state changes from loading to something else (because it is only returned in the loading state).
So... if we've changed from loading to idle before the promise has returned, when it returns the canceled flag will be true and it will return without doing anything.

I do however feel that this logic kind of goes against what this entire post is trying to advocate: declarative, easy to understand logic.

I'm wondering if maybe there's a more "state machiney" way to implement this functionality (without going full on state machine like in the last example)

Collapse
 
sapegin profile image
Artem Sapegin

I also stumbled over this example and agree that explicit cancelation would make the app logic easier to understand. Implicit cancelation feels too close to the isLoading from the very first example.

Collapse
 
davidkpiano profile image
David K. 🎹

Yes there is, and Reason has done it - reasonml.github.io/reason-react/do...

Collapse
 
uriklar profile image
Uri Klar

Same question, but regarding the XState example.
What makes the cancelation logic work? Does the onDone function not get invoked if the src promise has resolved but we have since transitioned to a different state?

Collapse
 
gmaclennan profile image
Gregor MacLennan

I had this question too, and eventually found the answer in the docs

If the state where the invoked promise is active is exited before the promise settles, the result of the promise is discarded.

The invoke property seems like a little bit of "magic" in XState and it took me a while to understand what is actually happening there.

David, thanks for writing this, having a concrete example really helped understand XState and I look forward to seeing more.

Collapse
 
gafemoyano profile image
Felipe Moyano

Thanks for the article David, it was very well thought out. I was wondering how this approach would work when using something like Apollo's useQuery hook to fetch data.

My initial approach was to assume my component would start on a 'loading' state. It might not be necessarily true, but it seems to work since the first pass of the render cycling useQuery will return a loading value set to true.

useQuery provides a prop for an onComplete function, so that seemed like a good place to call dispatch({type: "RESOLVE", data}) and let the reducer do some work and put the data into the state.
And this seemed to work fine for the most part. However, I bumped into a problem when some other component updated data via mutation. Turns out that onComplete will, understandably, only run the first time the query is completed. But apollo apparently does some magic to notify data that something mutated it, updates it, and triggers a render.

The example goes something like this:
You get a user and its credit cards from use query:

const {loading, data, error} = useQuery()
// data.user = { user:  {id: 1, creditCards: []} 
Then somewhere else the user adds a credit card via useMutation()
// magically data.user is now { user: {id: 1, creditCards: [{id:1}] }
Enter fullscreen mode Exit fullscreen mode

So even though I could send the newly added credit card on a dispatch call, and update the state accordingly, it kind of feels like i'd be maintining two sources of truth. Whatever apollo's useQuery returns and what I've manually placed on the Store.

Anyways, all of this is to say... how would you make this work with Apollo? Are the approaches at odds, or am I making the wrong kind of assumptions on how to handle the response?

Cheers, and thanks again for writing this up.

Collapse
 
karfau profile image
Christian Bewernitz

We also had this question recently and decided to decouple queries and mutations from the state machines where possible.
In the end the appollo hooks implement their own "state machines" in a way.

It's an ongoing process to convert the existing code, but we are convinced that it's the right approach for us.

Collapse
 
savagepixie profile image
SavagePixie

This is an amazing article! Very well written and loads of food for thought. If nothing else comes out of this, at least you've helped me finally grasp what redux is trying to accomplish. So thanks for that.

My only criticism is that your app fetches dog photos instead of cat photos.

Collapse
 
davidkpiano profile image
David K. 🎹

(time to add a cat-themed easter egg to the demo...)

Collapse
 
yuriykulikov profile image
Yuriy Kulikov • Edited

Please never use state machines for UI. It is a terrible idea proven to me by 3 big projects (1-10 million LOC) which have done that.

Dog fetching issue can be solved with RxJS switchMap.

Collapse
 
davidkpiano profile image
David K. 🎹

Lol you're already using state machines. RxJS operators and observables are state machines.

The state explosion problem is solved with hierarchical states, which XState supports.

Collapse
 
yuriykulikov profile image
Yuriy Kulikov

Not everything stateful is a state machine. I all for hierarchical state machines (actually I use them in most of my projects and I even have an open source library for HFSM). But it is not applicable for every task at hand. User Interface is one thing which rarely can be implemented with a SM in a maintainable way.

Thread Thread
 
davidkpiano profile image
David K. 🎹

Tell that to the many developers using state machines in user interfaces already (for years) with great success.

Of course it's not applicable for every use-case, but saying it's rarely useful without evidence is not helpful.

Thread Thread
 
macsikora profile image
Pragmatic Maciej • Edited

Nobody argues that every app form a state machine. The argue is should it be explicit or implicit one. I am for making not possible state not possible, but I see precise types (sums) as a tool for achieving the most. Runtime FSM looks like overcomplicating the problem.

Thread Thread
 
davidkpiano profile image
David K. 🎹

That's fine, my goal is to get developers thinking about their apps in terms of finite states and preventing impossible states. It's up to you whether you want to use a runtime FSM or not.

Collapse
 
savagepixie profile image
SavagePixie

Would you mind sharing the highlights of your experience with those three projects that made you realise state machines were a bad idea for UIs?

Collapse
 
yuriykulikov profile image
Yuriy Kulikov

I have to be careful with the details, so I will only summarize what is already available to the public. I have participated in several projects for major car manufacturers. Three projects were built using a UI framework, which was based on a HFSM. There were a lot of states (a lot!) and the interfaces themselves were quite sofisticated. Many external factors were also taken into account, for example what happens if the car moves and some functionality must be disabled. These projects had from 1 to 10 million LOC in Java just to feed the HFSM with events.
Despite good tooling (visualization), it was an unmaintainable mess. State machine did not actually made the code more maintainable, mostly because state machine was a bad model for the UI. In the end there were clusters of states which were interconnected in every possible way. There were dedicated developers who had to maintain this state machine. I was lucky to participate in another project, which has a very similar user interface (you wouldn't tell the difference), but this time without any tooling or an abstraction for the UI navigation. Well, it was also a mess with your usual "of click here, go there, unless there is disabled, then go elsewhere". But it was actually much, much better. We have experimented with other approaches. I personally find decision trees very practical if the interface is dynamic and has to take a lot of external events into account. And it always makes sense to make user interface hierarchical, for example using nested routing outlets. For simple UI you can get away with a simple backstack.

Collapse
 
juliang profile image
Julian Garamendy • Edited

Thank you so much for writing this!

I've been keeping an eye on xstate, waiting for an opportunity to use it in a real project.
This example of fetching in React was all I needed (so I wouldn't have to figure it out myself)

BTW I think the link to @xstate/react is wrong.

BRB! I have a buggy application to fix with xstate.

Collapse
 
davidkpiano profile image
David K. 🎹

Thanks, fixed!

Collapse
 
mrvaa5eiym profile image
mrVAa5eiym

Hi all what about this? am I missing something?

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();

    if (state.status === 'loading') {
      return;
    }

    dispatch({ type: ActionType.sentData });

    try {
      await axios.request({
        // some code
      });

      dispatch({ type: ActionType.success });
    } catch (error) {
      dispatch({ type: ActionType.error });
    }
  }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
marbiano profile image
Martin Bavio

I'm literally like this right now: 🤯

Thank you for this article, it's amazing how much mental models can influence what we feel like it's good code or not.

Collapse
 
tylerlwsmith profile image
Tyler Smith

This was really cool! I'm gonna need to read this a few more times before I really understand the state machine part, but I have it bookmarked. I'm gonna be working on my most complicated React app I've ever written in three weeks so I should probably start studying up on this.

Collapse
 
jamesta696 profile image
Jamie Smith

I just seen a ton of code just to fetch dogs on a click, this can be done so much simpler with less code using native JS.

Mixing JS with HTML seems laughable.
I appreciate your article though.
These frameworks and libraries are getting more bloated by the weeks going by.

Collapse
 
trjones1 profile image
Tramel Jones

Great write-up. Thanks for including the buggy examples to play with too!

Collapse
 
eddyvinck profile image
Eddy Vinck

Thanks for the reminder. I really need to look into state machines sometime :)

Collapse
 
pavelloz profile image
Paweł Kowalski

Great article. Ill keep it in my favs. I have a feeling i will circle back to it every now and then. :)

Collapse
 
oscargm profile image
Óscar Garcia

That's the kind of example I was looking for!

I have a similar situation, but with a lot more complexity in which i want to bring state machines and this brings me some light! Thanks!

Collapse
 
illourr profile image
Dillon Curry

Nice article David.
You never cleanup the Error message in the state charts example, looks like you need to set error to null on entry of the loading state transition.

Collapse
 
daviddalbusco profile image
David Dal Busco

Thank you for the great blog post and clear conclusion 👍

P.S.: The Dog API is my new favorite API for blog post too, such a good API 🐶

Collapse
 
cookavich profile image
Paul Cook

Good stuff, glad you officially published it :D

Collapse
 
benbot profile image
Benjamin Botwin

Wow this article blew my mind and introduced me to Xstate.

State machines are used all the time in game dev. I can’t believe I never thought of using them for UI

Collapse
 
joaomelo profile image
joão

so beautifully written with advanced concepts presented so pedagogically. i will keep this article in my memory and heart for a long time. thanks and congratulation.

Collapse
 
nickytonline profile image
Nick Taylor

Great post David! I've been reading about XState and saw your stream with Jason Lengstorf. I just need to make something with it. 😉

Side note, but keep up the great work with Keyframers!

Collapse
 
kettanaito profile image
Artem Zakharchenko

Great article, David! It's been super useful to read through the analyzis and suggestions. Hope to read more from you in the future!

Collapse
 
ovchinnikovdev profile image
Konstantin

Great job!

Collapse
 
jasonawise profile image
Jason A. Wise

This is awesome! Thanks for sharing!

Collapse
 
vraa profile image
Veera

API first, UI next.

Collapse
 
avgcr profile image
AVGCR

Well, now we have longer code than it should

Collapse
 
davidkpiano profile image
David K. 🎹

Go ahead, write shorter, buggier code.