useStateMachine: A ½ kb state machine hook for React
github.com
github.com
They encapsulate logic so well and do an amazing job being this isolated data/event source.
I find that I reuse them far more than I reuse components.
But they're also soooo idiosyncratic which makes them rather ugly on a general level.
But yes, insanely useful once you get the hang of them, they enable forms of abstraction and composability that you'll sorely miss when working in other UI frameworks.
* If there’s only useState, then everything is mostly fine. All is good.
* To avoid unnecessary re-rendering you’ll have to move callbacks into useCallback. These callbacks then also need to specify all of their dependencies. So many lines of code that are merely noise.
* They encourage having state locally in the component which often breaks down the moment you slightly tweak the design. So much refactoring!
* Dealing with any other (callback-based, stateful) API is confusing. If you want something to execute once initially you can use useEffect with an empty dependency list, but note that then there’s no way of accessing updates values (in callbacks) and everything will reflect the initial state. Often you’ll have to use useRef just to keep track of the current state. Or maybe useState?
* Debugging is painful. You forget one value in your dependency list and the weirdest things happen. Suddenly you have callbacks running in different rendering scopes with different values.
They feel very “brittle”: Once it works the code looks pretty, but when there’s a slight change of requirements you need to rethink everything.
I really like Vue 3’s Composition API as a much easier-to-use (IMO) implementation of hooks.
It’s conceptually very similar to React Hooks, but the method that defines them only runs once, when the component is instantiated. So straight away that removes any worries around conditionals and loops.
Instead of a plain value/object and an update function, the “hooks” instead return a reactive object that tracks it’s own dependencies (which are also all reactive objects). That removes any need to manage dependencies yourself, or for any memoization.
The only downside is that because a plain value (like a number or something) cannot be reactive, it has to be wrapped in an object and accessed via a “.value” property, but this is only relevant in the setup function. Anywhere else, like the Options API or in the template, accessing the value like a value will just get transparently proxied to the object.
Don't you get eslint warnings right in your editor?
If you're using VS Code there's an extension for that...
I'm not sure if I understood what you mean, but note that useState accepts not only the updated state, but also a function that takes the current state as argument and return the updated state.
In other words, instead of something like this
useEffect( async () => {
...
const someValue = await someFunction();
setState({...state, someValue });
}, [... ] );you should use something like this
useEffect( async () => {
...
const someValue = await someFunction();
setState( currentState => ({...currentState, someValue }));
}, [ ... ] );(the example is with async but it is the same for regular callbacks)
useEffect(() => {
const interval = setInterval(myFunc, 1000)
return () => clearInterval(myInterval)
},[])
I think one of the keys to React is not to fear the boilerplate.If you find yourself typing the same code too often, there’s probably a higher level abstraction that you should find, way above the level of setInterval.
I find coders often try to DRY up low level code to the detriment of actually dealing with their _application_ architecture.
In general, you should use the tools as provided, boilerplate and all, and then focus your real brain energies on the specific domain problems that only your application needs.
Creating thin wrappers on external tools to squeeze the last tiny bits of DRYness is not only usually a waste of time, but it makes your code unreadable. People know how setInterval and useEffect work. If I see useInterval I have to go read your code to know what’s happening. Especially if you made it configurable. And people _will_ tend to add configuration to these wrappers over time as needs change.
- Agreed about useState (and useReducer). They’re simple and they get the job done.
- Agreed about useCallback as well, it’s a lot of boilerplate.
- State in general is a hard problem to deal with, but I’m not sure how hooks in particular encourage it? Class components had the same issue.
- IMO, useEffect is a huge footgun. I understand how it works, I understand how closures work, but it is by far the biggest source of hook-related bugs I’ve experienced.
- This eslint plug-in will solve your dependency list woes: https://www.npmjs.com/package/eslint-plugin-react-hooks
This is what I hate in angular1 , there are at least 3-4 ways you can mess up a simple data-binding and there is no error or warning to inform you that something is not right so you waste a lot of time to figure it out.
I used react a few years back but it seems that this days it got hyper complex.
Linters should help with missing dependencies.
Really? I've been working with hooks for months, with code in production, and I hardly ever pass more than one or two items to the dependency list. The only time I really had any trouble with dependencies was when I was updating appContext inside a hook.
There's an eslint plugin specifically for detecting this, which should probably be considered mandatory.
This should never happen. The only downside to hooks is that the eslint plugin is _required_, not optional. It is not worth the pain without it, but you'll be in a Whole New World once you do it. Since you can auto-fix every time, it'll become second nature and you'll never forget a dependency again. You can focus on getting your application to work.
I’m curious what you think of https://rxstore.dev/ it basically is a pattern I tried to outline where you write logic with rxjs and it tries to bridge the gap to react/hooks for you. I address some of your points in the faq.
This is by design. When you start making a component worry about what it needs to do on an update, rather than just rendering its data, you are introducing a whole new level of complexity. That's why class based React components could turn into these unwieldy confusing things where you never knew for certain what was going to render every time, because the shouldComponentUpdate method gets stuffed full of business logic.
There's something to having a function that can hold state or load its state on mount, etc.
You're no longer writing plain javascript, you're writing in some terribly verbose language where everything from variable declarations to function expressions needs to be wrapped, making it hard to integrate with this party APIs and small details can have a big impact on performance.
Both vue 3 and svelte show it can be done better.
You can definitely have "hooks" that are just normal js functions without limits.
My favourite public example of this is definitely solid.js (by ryansolid).
In general, I'm grateful to react / facebook for bringing in the spotlight the hyperscript way of doing applications and pushing tons of developers to develop similar libraries - but technically react is bloated and a ugly corporate mess.
Similar considerations for redux and elm.
The problem with state machines in Java is not the state machine's fault, it's Java's fault.
It’s a little verbose more my liking but I think it could be powerful for large apps with lots of states.
Types of examples I am not interested in seeing:
- a DropDown list using a state machine (or something similarly simple that’s been solved a million times in a simple way).
Edit:
I just think stuff like this is too weird and dangerous to evangelize, and I’m at my wits end in dealing with simple components that have been over abstracted.
It seems like you're just unfamiliar with state machines. They're not a "new, weird abstraction", they're a core concept of computer science in general, and it can be argued that most code we write is itself an abstraction on top of state machines; not the other way around.
For more information on state machines and statecharts, I recommend reading this great resource: https://statecharts.dev/
From your examples: https://xstate-catalogue.com/machines/confirmation-dialog
I’ve written a million confirm-dialogs in my lifetime, and that example is one of the most convoluted things I’ve ever seen.
Guys, just because it’s in Computer Science, doesn’t automatically mean it’s a more sophisticated solution. The term for this is over-kill.
EDIT: Okay, didn't mean to interpret criticism as negativity; I apologize.
So, in a lot of cases in the UI, we are turning what is very recognizable and traditionally understood code into a new form to fit the state-machine world view in a more 1:1 way.
Again, it’s a trade off. What did we gain from this transformation? I don’t think we get enough back from this trade to structure our code in this new way.
Or to be clear, why would I trade my ability to read a few simple fetch calls that update a simple DOM element for this version? What is the outsized return here?
Just my 2 crypto.
I think using state machines is just a very different way of looking at code, and it does take some mind bending to think that way; but once you do, it's much easier to blend the code together the same way you'll see imperative and functional code blended together these days and not think twice about it.
However, some things just _are_ state machines. You don’t usually see them in the GUI (except for example wizards), but many parsers can trivially be implemented with state machines.
You're saying that he doesn't know computers? Computers are state machines. Presumably he used one to post his comment.
My initial defensive impulse to this topic was mostly to take a stand against frontend code that is becoming increasingly obtuse over the years.
And you can just write this stuff as basically lots of nested if statements but...
As an actual example, I have a state machine in an app I work on:
1. check if there's a session (if so goto 4). 2. If not go to email input. 3. On submit go to password input. 4. authorized. However: 2 and 3 and some of the time 1 have network requests, so handle those. Password can be entered wrong three times (error message changes accordingly, oh also no. retries are modifiable at admin level), which bumps user back to 2 (error message changes accordingly). User may have turned on PIN security, if so that will be an extra step before allowed into app. They may also have it turned on, but not one set, which is a slightly different flow. And they can turn it off/on from within app. Oh and biometric, same deal as PIN. Also can drop back to username/pword instead of OTP in some circumstances.
That's most stuff I think off top of head. That's a hierarchical state machine written using XState. It's easy to test (UI is seperate from the machine, and I can just plug in API functions for the remote calls). It's relatively easy to read (xstate API produces stuff that reads like a diagram, it's easy to explain what's going on). It's easy to add/remove states
There are other potential uses for in my apps (ex. backing a numeric input made up of multiple inputs for PIN/OTP that needs to track focus state + a few other things), but I've been slightly wary about introducing more xstate code.
I feel like:
- explicit state machines are extremely useful.
- they're very well understood by embedded/game devs, but not so much by web GUI devs...
- ...but the latter have an innate understanding due to much of the things they build often being implicit state machines. And those things are often fine as they are.
- adding an explicit machine greatly increases granular control at the expense of [complex] boilerplate.
- the simple fact that they are so explicit (and that you get one state at a time) means state machines aren't some silver bullet for UI, and they get complicated really fast due to state explosion.
- I think any game dev would be able to explain why you don't just use them for everything, and I find it slightly strange that there's little input from game/embedded devs when these things come up on HN -- maybe I'm just assuming there's some overlap of knowledge when actually there isn't.
- heirarchical state machines and state charts fix the state explosion issue, but then bring with them a more complex API that needs to be learnt.
I'd say, for web/JS, XState is gold standard and it's a fantastic library. Explicit state machines are very useful, but writing ad-hoc machines that do anything useful isn't particularly pleasant in JS; XState, with its serialisable object configuration, is pretty easy to explain.
It has the advantage of being completely agnostic w/r/t front-end library, which I think is critical. I can shove it behind an interface, and if necessary replace it with something else pretty easily. I'm somewhat uneasy about having it directly tied to [in the OP's case] React, because I'm in agreement that it's baking clever, complex code right in (yes, for anyone disagreeing, I am aware state machines are a simplification. So is Redux. So are observables. Hell, so is recursion. Anyway).
Here's hopefully a more apt example: imagine wanting to have a multiple document interface (MDI) that shows a bunch of 'window' like views. Some examples of the idea are react-grid-layout, golden-layout, react-mosaic. Each window can be minimized, maximized, moved, flashed, and closed ... all with flashy animations. You could create a whole bunch of components that allow you to capture all the different states and toggle them in different components, and effectively have the MDI business logic sprinkled all through out YOUR code, not just the 'library' code. Alternatively, you could use a state machine that captures all the states in a single spot and manages that all efficiently and safely, you just call an api to trigger state changes throughout your code.
As some other comments have mentioned, this is a fundamental part of computer science. You can get by without knowing it cause you can write a bunch of ifs and state logic all over the place. If you only know how to use a hammer, ya you could still hammer a screw in. Understanding how different data structures of different patterns fit together provides you new tools to do things in different ways when appropriate. Learn how to manage complexity, not fear it, cause there's nothing dangerous about state machines. In fact, I'd argue that anyone that thinks state machines make things more complex just isn't looking at a large enough scale because state machines should make things less complex by wrapping all the complexity inside them (when used appropriately).
Sporadic business logic/spaghetti code is a problem in any application. State machines will not magically avoid this. In fact, it should be just as susceptible to it. When sphagetti code shows up under a complex architecture like this, you could be in a world of hurt. There won’t be a few if-statements for you to unwrap, but instead a maze of cascading state updates. Another common thing I’ve seen is the granularity of capturing any and all state updates, and then some. It’s very tedious.
Anyway, I’ve been dead wrong about 70% of things in life before, so I’d be happy to look at a non trivial app written with xstate if anyone’s got a repo.
Allow popups to see a live visualization of the statechart.
I am using state machines to write reactive applications: https://brucou.github.io/documentation/
So far I have to say that it is a mixed experience. There is a cost to the abstraction and the indirection (that is fairly well known easy to describe even if we rarely do so due to some self-imposed no-negativity bias or having some interest in the game), and then there are the benefits that are less easy to describe because they depend highly on the nature of the problem that you are addressing.
I implemented a Medium clone application (https://codebase.show/projects/realworld) with state machines. The result is 47Kb (brought down to 39Kb after compiling the machine and other optimizations) vs. 70Kb for the Vue implementation or 160 KB for the React/Redux one (that implementation piles abstraction over abstraction in the form of libraries and pays the corresponding price). I would count that as benefit. Also cf. https://brucou.github.io/documentation/v1/tutorials/index.ht... for the full pitch.
The high-level machine is that one: https://brucou.github.io/documentation/graphs/real-world/rea...
At this level, you can follow the routing of the app. Every route is a compound state. If you open it, you see the details of the behavior. The full machine (post refactoring) is like this: https://brucou.github.io/documentation/graphs/real-world/rea...
So one advantage is that with those graphs, it is easier to onboard new folks arriving to the codebase. They just have to follow arrows to know what the code is doing in response to a series of inputs.
I could continue but with all that said, the Hyperapp implementation of the same application is 27Kb and also fairly simple (even if arguably not as simple) to get into. So there isn't a clear-cut ex-nihilo benefits to state machines here. In fact if you would have implemented the same application as a MPA instead of a SPA, for most, if not all of the pages, using a state machine to model the behavior is simply overkill. Most of the job and value of the machine in my example is to do the client-side routing done in the machine (so no extra library cost).
Anyways the bottom line is the tool is useful but you have to figure for what, and where is the value maximized. The obvious cases have already been figured out.