Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

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

Posted on • Edited on

     

No, disabling a button is not app logic.

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:

User interface development tools are very powerful. They can be used to construct large and complex user interfaces, with only a relatively small amount of code written by an application developer. And yet, despite the power of such tools and the relatively small amount of code that is written, user interface software often has the following characteristics:

  • the code can be difficult to understand and review thoroughly:
  • the code can be difficult to test in a systematic and thorough way;
  • the code can contain bugs even after extensive testing and bug fixing;
  • the code can be difficult to enhance without introducing unwanted side-effects;
  • the quality of the code tends to deteriorate as enhancements are made to it.

Despite the obvious problems associated with user interface development,little effort has been made to improve the situation. Any practitioner who has worked on large user interface projects will be familiar with many of the above characteristics, which aresymptomatic of the way in which the software is constructed.

In case you didn't do the math, this was writtenover 20 years ago and yet it echoes the same sentiments that many developers feel today about the state of app development. Why is that?

We'll explore this with a simple example: fetching data in a React component. Keep in mind, the ideas presented in this article are not library-specific, nor framework-specific... in fact, they're not even language specific!

Trying to makefetch() happen

Suppose we have aDogFetcher component that has a button that you can click to fetch a random dog. When the button is clicked, aGET request is made to theDog API, and when the dog is received, we show it off in an<img /> tag.

A typical implementation withReact Hooks might look like this:

functionDogFetcher(){const[isLoading,setIsLoading]=useState(false);const[dog,setDog]=useState(null);return(<div><figureclassName="dog">{dog&&<imgsrc={dog}alt="doggo"/>}</figure><buttononClick={()=>{setIsLoading(true);fetch(`https://dog.ceo/api/breeds/image/random`).then(data=>data.json()).then(response=>{setDog(response.message);setIsLoading(false);});}}>{isLoading?"Fetching...":"Fetch dog!"}</button></div>);}
Enter fullscreen modeExit fullscreen mode

This works, but there's one immediate problem: clicking the button more than once (while a dog is loading) will display one dog briefly, and then replace that dog with another dog. That's not very considerate to the first dog.

The typical solution to this is to add adisabled={isLoading} attribute to the button:

functionDogFetcher(){// ...<buttononClick={()=>{// ... excessive amount of ad-hoc logic}}disabled={isLoading}>{isLoading?"Fetching...":"Fetch dog!"}</button>// ...}
Enter fullscreen modeExit fullscreen mode

This also works; you're probably satisfied with this solution. Allow me to burst this bubble.

What can possibly go wrong?

Currently, the logic reads like this:

When the button is clicked, fetch a new random dog, and set a flag to make sure that the button cannot be clicked again to fetch a dog while one is being fetched.

However, the logic youreally want is this:

When a new dog is requested, fetch it and make sure that another dog can't be fetched at the same time.

See the difference? The desired logic is completely separate from the button being clicked; it doesn't matterhow the request is made; it only matters what logic happens afterwards.

Suppose that you want to add the feature that double-clicking the image loads a new dog. What would you have to do?

It's all too easy to forget to add the same "guard" logic onfigure (after all,<figure disabled={isLoading}> won't work, go figure), but let's say you're an astute developer who remembers to add this logic:

functionDogFetcher(){// ...<figureonDoubleClick={()=>{if(isLoading)return;// copy-paste the fetch logic from the button onClick handler}}>{/* ... */}</figure>// ...<buttononClick={()=>{// fetch logic}}disabled={isLoading}>{/* ... */}</button>// ...}
Enter fullscreen modeExit fullscreen mode

In reality, you can think about this as any use-case where some sort of "trigger" can happen from multiple locations, such as:

  • a form being able to be submitted by pressing "Enter" in an input or clicking the "Submit" button
  • an event being triggered by a user actionor a timeout
  • any app logic that needs to be shared between different platforms with different event-handling implementations (think React Native)

But there's a code smell here. Our same fetch logic is implemented in more than one place, and understanding the app logic requires developers to jump around in multiple parts of the code base, finding all of the event handlers where there are tidbits of logic and connecting them together mentally.

DRYing up the splashes of logic

Okay, so putting logic in our event handlers is probably not a good idea, but we can't exactly put our finger on the reason why yet. Let's move the fetch logic out into a function:

functionDogFetcher(){const[isLoading,setIsLoading]=useState(false);const[dog,setDog]=useState(null);functionfetchDog(){if(isLoading)return;setIsLoading(true);fetch(`https://dog.ceo/api/breeds/image/random`).then(data=>data.json()).then(response=>{setDog(response.message);setIsLoading(false);});}return(<div><figureclassName="dog"onDoubleClick={fetchDog}>{dog&&<imgsrc={dog}alt="doggo"/>}</figure><buttononClick={fetchDog}>{isLoading?"Fetching...":"Fetch dog!"}</button></div>);}
Enter fullscreen modeExit fullscreen mode

Adding features and complexity

Now let's see what happens when we want to add basic "features", such as:

  • If fetching a dog fails, an error should be shown.
  • Fetching a dog should be cancellable.

I hesitate to call these "features" because these types of behaviors should be naturally enabled by the programming patterns used, but let's try to add them anyhow:

functionDogFetcher(){const[isLoading,setIsLoading]=useState(false);const[error,setError]=useState(null);const[canceled,setCanceled]=useState(false);const[dog,setDog]=useState(null);functionfetchDog(){setCanceled(false);setError(null);setIsLoading(true);fetchRandomDog().then(response=>{// This should work... but it doesn't!if(canceled)return;setIsLoading(false);setDog(response.message);}).catch(error=>{setIsLoading(false);setCanceled(false);setError(error);});}functioncancel(){setIsLoading(false);setCanceled(true);}return(<div>{error&&<spanstyle={{color:"red"}}>{error}</span>}<figureclassName="dog"onDoubleClick={fetchDog}>{dog&&<imgsrc={dog}alt="doggo"/>}</figure><buttononClick={fetchDog}>{isLoading?"Fetching...":"Fetch dog!"}</button><buttononClick={cancel}>Cancel</button></div>);}
Enter fullscreen modeExit fullscreen mode

Thislooks like it should work -- all of our Boolean flags are being set to the correct values when things happen. However,it does not work because of a hard-to-catch bug:stale callbacks. In this case, thecanceled flag inside the.then(...) callback will always be the previous value instead of the latestcanceled value, so cancelling has no effect until the next time we try to fetch a dog, which isn't what we want.

Hopefully you can see that even with these simple use-cases, our logic has quickly gone out-of-hand, and juggling Boolean flags has made the logic buggier and harder to understand.

Reducing complexity effectively

Instead of haphazardly adding Boolean flags everywhere, let's clean this up with theuseReducer anduseEffect hooks. These hooks are useful because they express some concepts that lead to better logic organization:

  • TheuseReducer hook uses reducers, which return the next state given the current state and some event that just occurred.
  • TheuseEffect hook synchronizes effects with state.

To help us organize the various app states, let's define a few and put them under astatus property:

  • An"idle" status means that nothing happened yet.
  • A"loading" status means that the dog is currently being fetched.
  • A"success" status means that the dog was successfully fetched.
  • A"failure" status means that an error occurred while trying to fetch the dog.

Now let's define a few events that can happen in the app. Keep in mind: these events can happen fromanywhere, whether it's initiated by the user or somewhere else:

  • A"FETCH" event indicates that fetching a dog should occur.
  • A"RESOLVE" event with adata property indicates that a dog was successfully fetched.
  • A"REJECT" event with anerror property indicates that a dog was unable to be fetched for some reason.
  • A"CANCEL" event indicates that an in-progress fetch should be canceled.

Great! Now let's write our reducer:

functiondogReducer(state,event){switch(event.type){case"FETCH":return{...state,status:"loading"};case"RESOLVE":return{...state,status:"success",dog:event.data};case"REJECT":return{...state,status:"failure",error:event.error};case"CANCEL":return{...state,status:"idle"};default:returnstate;}}constinitialState={status:"idle",dog:null,error:null};
Enter fullscreen modeExit fullscreen mode

Here's the beautiful thing about this reducer. It iscompletely framework-agnostic - we can take this and use it in any framework, or no framework at all. And that also makes it much easier to test.

But also, implementing this in a framework becomesreduced (pun intended) tojust dispatching events. No more logic in event handlers:

functionDogFetcher(){const[state,dispatch]=useReducer(dogReducer,initialState);const{error,dog,status}=state;useEffect(()=>{// ... fetchDog?},[state.status]);return(<div>{error&&<spanstyle={{color:"red"}}>{error}</span>}<figureclassName="dog"onDoubleClick={()=>dispatch({type:"FETCH"})}>{dog&&<imgsrc={dog}alt="doggo"/>}</figure><buttononClick={()=>dispatch({type:"FETCH"})}>{status==="loading"?"Fetching...":"Fetch dog!"}</button><buttononClick={()=>dispatch({type:"CANCEL"})}>Cancel</button></div>);}
Enter fullscreen modeExit fullscreen mode

However, the question remains: how do we execute the side-effect of actually fetching the dog? Well, since theuseEffect hook is meant for synchronizing effects with state, we can synchronize thefetchDog() effect withstatus === 'loading', since'loading' means that that side-effect is being executed anyway:

// ...useEffect(()=>{if(state.status==="loading"){letcanceled=false;fetchRandomDog().then(data=>{if(canceled)return;dispatch({type:"RESOLVE",data});}).catch(error=>{if(canceled)return;dispatch({type:"REJECT",error});});return()=>{canceled=true;};}},[state.status]);// ...
Enter fullscreen modeExit fullscreen mode

The fabled "disabled" attribute

The logic above works great. We're able to:

  • Click the "Fetch dog" button to fetch a dog
  • Display a random dog when fetched
  • Show an error if the dog is unable to be fetched
  • Cancel an in-flight fetch request by clicking the "Cancel" button
  • Prevent more than one dog from being fetched at the same time

... all without having to put any logic in the<button disabled={...}> attribute. In fact, we completely forgot to do so anyway, and the logic still works!

This is how you know your logic is robust; when it works, regardless of the UI. Whether the "Fetch dog" button is disabled or not, clicking it multiple times in a row won't exhibit any unexpected behavior.

Also, because most of the logic is delegated to adogReducer function definedoutside of your component, it is:

  • easy to make into a custom hook
  • easy to test
  • easy to reuse in other components
  • easy to reuse in otherframeworks

The final result

Change the<DogFetcher /> version in the select dropdown to see each of the versions we've explored in this tutorial (even the buggy ones).

Pushing effects to the side

There's one lingering thought, though... isuseEffect() the ideal place to put a side effect, such as fetching?

Maybe, maybe not.

Honestly, in most use-cases, it works, and it works fine. But it's difficult to test or separate that effect from your component code. And with the upcoming Suspense and Concurrent Mode features in React, the recommendation is to execute these side-effects when some action triggers them, rather than inuseEffect(). This is because the official React advice is:

If you’re working on a data fetching library, there’s a crucial aspect of Render-as-You-Fetch you don’t want to miss.We kick off fetching before rendering.

https://reactjs.org/docs/concurrent-mode-suspense.html#start-fetching-early

This is good advice. Fetching data should not be coupled with rendering. However, they also say this:

The answer to this is we want to start fetching in the event handlers instead.

This is misleading advice. Instead, here's what should happen:

  1. An event handler shouldsend a signal to "something" that indicates that some action just happened (in the form of an event)
  2. That "something" shouldorchestrate what happens next when it receives that event.

Two possible things can happen when an event is received by some orchestrator:

  • State can be changed
  • Effects can be executed

All of this can happen outside of the component render cycle, because it doesn't necessarily concern the view. Unfortunately, React doesn't have a built-in way (yet?) to handle state management, side-effects, data fetching, caching etc. outside of the components (we all know Relay is not commonly used), so let's explore one way we can accomplish this completely outside of the component.

Using a state machine

In this case, we're going to use a state machine to manage and orchestrate state. If you're new to state machines, just know that they feel like your typical Redux reducers with a few more "rules". Those rules have some powerful advantages, and are also the mathematical basis for how literally every computer in existence today works. So they might be worth learning.

I'm going to useXState and@xstate/react to create the machine:

import{Machine,assign}from"xstate";import{useMachine}from"@xstate/react";// ...constdogFetcherMachine=Machine({id:"dog fetcher",initial:"idle",context:{dog:null,error:null},states:{idle:{on:{FETCH:"loading"}},loading:{invoke:{src:()=>fetchRandomDog(),onDone:{target:"success",actions:assign({dog:(_,event)=>event.data.message})},onError:{target:"failure",actions:assign({error:(_,event)=>event.data})}},on:{CANCEL:"idle"}},success:{on:{FETCH:"loading"}},failure:{on:{FETCH:"loading"}}}});
Enter fullscreen modeExit fullscreen mode

Notice how the machine looks like our previous reducer, with a couple of differences:

  • It looks like some sort of configuration object instead of a switch statement
  • We're matching on thestate first, instead of theevent first
  • We're invoking thefetchRandomDog() promise inside the machine! 😱

Don't worry; we're not actually executing any side-effects inside of this machine. In fact,dogFetcherMachine.transition(state, event) is apure function that tells you the next state given the current state and event. Seems familiar, huh?

Furthermore, I can copy-paste this exact machine andvisualize it in XState Viz:

Visualization of dog fetching machine

View this viz on xstate.js.org/viz

So what does our component code look like now? Take a look:

functionDogFetcher(){const[current,send]=useMachine(dogFetcherMachine);const{error,dog}=current.context;return(<div>{error&&<spanstyle={{color:"red"}}>{error}</span>}<figureclassName="dog"onDoubleClick={()=>send("FETCH")}>{dog&&<imgsrc={dog}alt="doggo"/>}</figure><buttononClick={()=>send("FETCH")}>{current.matches("loading")&&"Fetching..."}{current.matches("success")&&"Fetch another dog!"}{current.matches("idle")&&"Fetch dog"}{current.matches("failure")&&"Try again"}</button><buttononClick={()=>send("CANCEL")}>Cancel</button></div>);}
Enter fullscreen modeExit fullscreen mode

Here's the difference between using a state machine and a reducer:

  • The hook signature foruseMachine(...) looks almost the same asuseReducer(...)
  • No fetching logic exists inside the component; it's all external!
  • There's a nicecurrent.matches(...) function that lets us customize our button text
  • send(...) instead ofdispatch(...)... and it takes a plain string! (Or an object, up to you).

A state machine/statechart defines its transitions from the state because it answers the question: "Which events should be handledfrom this state?" The reason that having<button disabled={isLoading}> is fragile is because we admit that some "FETCH" event can cause an effect no matter which state we're in, so we have to clean up our ~mess~ faulty logic by preventing the user from clicking the button while loading.

Instead, it's better to be proactive about your logic. Fetching should only happen when the app is not in some"loading" state, which is what is clearly defined in the state machine -- the"FETCH" event is not handled in the"loading" state, which means it has no effect. Perfect.

Final points

Disabling a button is not logic. Rather, it is a sign that logic is fragile and bug-prone. In my opinion, disabling a button should only be a visual cue to the user that clicking the buttonwill have no effect.

So when you're creating fetching logic (or any other kind of complex logic) in your applications, no matter the framework, ask yourself these questions:

  • What are the concrete, finite states this app/component can be in? E.g., "loading", "success", "idle", "failure", etc.
  • What are all the possible events that can occur, regardless of state? This includes events that don't come from the user (such as"RESOLVE" or"REJECT" events from promises)
  • Which of the finite states should handle these events?
  • How can I organize my app logic so that these events are handled properly in those states?

You do not need a state machine library (like XState) to do this. In fact, you might not even needuseReducer when you're first adopting these principles. Even something as simple as having a state variable representing a finite state can already clean up your logic plenty:

functionDogFetcher(){// 'idle' or 'loading' or 'success' or 'error'const[status,setStatus]=useState('idle');}
Enter fullscreen modeExit fullscreen mode

And just like that, you've eliminatedisLoading,isError,isSuccess,startedLoading, and whatever Boolean flags you were going to create. And if you really start to miss thatisLoading flag (for whatever reason), you can still have it, but ONLY if it's derived from your organized, finite states. TheisLoading variable should NEVER be a primary source of state:

functionDogFetcher(){// 'idle' or 'loading' or 'success' or 'error'const[status,setStatus]=useState('idle');constisLoading=status==='loading';return(// ...<buttondisabled={isLoading}>{/* ... */}</button>// ...);}
Enter fullscreen modeExit fullscreen mode

And we've come full circle. Thanks for reading.

Cover photo by Lucrezia Carnelos on Unsplash

Top comments(44)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
felix profile image
Felix Guerin
Front-end Web developer

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 replicateuseEffect() 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?

CollapseExpand
 
adam_cyclones profile image
Adam Crockett 🌀
How’s it going, I'm a Adam, a Full-Stack Engineer, actively searching for work. I'm all about JavaScript. And Frontend but don't let that fool you - I've also got some serious Backend skills.
  • Location
    City of Bath, UK 🇬🇧
  • Education
    11 plus years* active enterprise development experience and a Fine art degree 🎨
  • Work
    Web Development Consultant at ForgeRock
  • Joined

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 🥳

CollapseExpand
 
adam_cyclones profile image
Adam Crockett 🌀
How’s it going, I'm a Adam, a Full-Stack Engineer, actively searching for work. I'm all about JavaScript. And Frontend but don't let that fool you - I've also got some serious Backend skills.
  • Location
    City of Bath, UK 🇬🇧
  • Education
    11 plus years* active enterprise development experience and a Fine art degree 🎨
  • Work
    Web Development Consultant at ForgeRock
  • Joined

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

CollapseExpand
 
uriklar profile image
Uri Klar
  • Location
    Israel
  • Work
    Front End Developer at Honeybook
  • Joined

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!

CollapseExpand
 
uriklar profile image
Uri Klar
  • Location
    Israel
  • Work
    Front End Developer at Honeybook
  • Joined

Ok, so after debugging the sandbox a bit I think I get it...
The cleanup function (that turnscanceled intotrue) only runs when state changes fromloading to something else (because it is only returned in the loading state).
So... if we've changed fromloading toidle 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)

CollapseExpand
 
sapegin profile image
Artem Sapegin
Coffee first developer, award-losing photographer, occasional leathercrafter, and dreamer of a boring life.
  • Location
    There
  • Joined

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 theisLoading from the very first example.

CollapseExpand
 
davidkpiano profile image
David K. 🎹
I play piano.
  • Location
    Orlando, Florida
  • Work
    Software Engineer at Stately
  • Joined

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

CollapseExpand
 
uriklar profile image
Uri Klar
  • Location
    Israel
  • Work
    Front End Developer at Honeybook
  • Joined

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

CollapseExpand
 
gmaclennan profile image
Gregor MacLennan
  • Joined

I had this question too, and eventually found the answerin the docs

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

Theinvoke 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.

CollapseExpand
 
yuriykulikov profile image
Yuriy Kulikov
  • Joined
• Edited on• 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.

CollapseExpand
 
davidkpiano profile image
David K. 🎹
I play piano.
  • Location
    Orlando, Florida
  • Work
    Software Engineer at Stately
  • Joined

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.

CollapseExpand
 
yuriykulikov profile image
Yuriy Kulikov
  • Joined

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. 🎹
I play piano.
  • Location
    Orlando, Florida
  • Work
    Software Engineer at Stately
  • Joined

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
I am Software Developer, currently interested in static type languages (TypeScript, Elm, ReScript) mostly in the frontend land, but working actively in Python also. I am available for mentoring.
  • Email
  • Location
    Lublin
  • Education
    M.Sc. Lublin University of Technology
  • Work
    Developer at DataArt
  • Joined
• Edited on• 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. 🎹
I play piano.
  • Location
    Orlando, Florida
  • Work
    Software Engineer at Stately
  • Joined

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.

CollapseExpand
 
savagepixie profile image
SavagePixie
Always learning new things. I love web development and coding in general.
  • Joined

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?

CollapseExpand
 
yuriykulikov profile image
Yuriy Kulikov
  • Joined

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.

Thread Thread
 
anthonyj25 profile image
Anthony
  • Joined
• Edited on• Edited

How about using statecharts in these huge projects instead of state machines? Reference:statecharts.dev/how-to-use-statech...

CollapseExpand
 
gafemoyano profile image
Felipe Moyano
  • Joined

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 modeExit 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.

CollapseExpand
 
karfau profile image
Christian Bewernitz
🏳‍🌈 white non-trans* he/him,born GDR/Europe,father of two sons, married
  • Location
    Saxony/Germany
  • Pronouns
    he/him
  • Work
    Software Engineer at Bettermarks GmbH
  • Joined

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.

CollapseExpand
 
savagepixie profile image
SavagePixie
Always learning new things. I love web development and coding in general.
  • Joined

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.

CollapseExpand
 
davidkpiano profile image
David K. 🎹
I play piano.
  • Location
    Orlando, Florida
  • Work
    Software Engineer at Stately
  • Joined

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

CollapseExpand
 
mrvaa5eiym profile image
mrVAa5eiym
  • Joined

Hi all what about this? am I missing something?

asyncfunctionhandleSubmit(event:FormEvent<HTMLFormElement>){event.preventDefault();if(state.status==='loading'){return;}dispatch({type:ActionType.sentData});try{awaitaxios.request({// some code});dispatch({type:ActionType.success});}catch(error){dispatch({type:ActionType.error});}}
Enter fullscreen modeExit fullscreen mode
CollapseExpand
 
marbiano profile image
Martin Bavio
Man in search of meaning.
  • Location
    Argentina
  • Work
    Senior UI Engineer at Marbiano Labs
  • Joined

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.

CollapseExpand
 
tylerlwsmith profile image
Tyler Smith
I'm a software developer who writes about Laravel, JavaScript, Rails, Linux, Docker, WordPress and the tech industry. Follow me on Twitter @tylerlwsmith
  • Location
    Sacramento, California
  • Education
    BA in Communications Emphasis in Public Relations
  • Work
    Software Engineer
  • Joined

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.

CollapseExpand
 
trjones1 profile image
Tramel Jones
  • Joined

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

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

I play piano.
  • Location
    Orlando, Florida
  • Work
    Software Engineer at Stately
  • Joined

More fromDavid K. 🎹

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp