[HN Gopher] useStateMachine: A 1/2 kb state machine hook for React
___________________________________________________________________
useStateMachine: A 1/2 kb state machine hook for React
Author : nkjoep
Score : 206 points
Date : 2021-05-21 07:01 UTC (1 days ago)
(HTM) web link (github.com)
(TXT) w3m dump (github.com)
| wyuenho wrote:
| Isn't the useReducer hook already an interface for building state
| machines? The clue is in the signature of reducer functions, the
| signature as exactly the same as textbook state transition
| functions...
| sudhirj wrote:
| The general use reducer is a infinite state machine, while this
| is a finite state machine. The normal reducer can allow your
| app to be in any state, while finite state machines let you
| list the only possible states (like ON, OFF, UNKNOWN; or
| DRAFT,SENT,BOUNCED) - and offers state transition functions to
| move between them that can refuse to move unless certain
| conditions are met.
| bennyg wrote:
| This still mostly sounds like a useReducer. With TypeScript
| you can guarantee the built code only respects a finite state
| shape. Depending on the dispatched action, you can just
| default to returning the previous state - refusing to move
| unless certain conditions are met.
| davidkpiano wrote:
| Yes, you can. The vast majority of developers don't. That's
| the difference: enforcement.
| wyuenho wrote:
| It's only a FSM if the users litter a guard function all over
| the place, otherwise this is just some deeply nested non-
| sugar on top of useReducer.
| Waterluvian wrote:
| I can't get over how amazing hooks are and how resistant I
| initially was to them.
|
| 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.
| judofyr wrote:
| Interesting. I have the opposite experience, the more I use
| them the more clunky they feel.
|
| * 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.
| [deleted]
| jakelazaroff wrote:
| My experience is mixed but mostly positive.
|
| - 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
| ketzo wrote:
| +1 for that eslint plugin, I was just idly wondering if
| something like that existed earlier this week. Thanks.
| jakelazaroff wrote:
| Unless your app is peculiar in some way I'd recommend
| using something like Create React App, which includes it
| automatically. https://create-react-app.dev
| deergomoo wrote:
| > 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.
|
| 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.
| nightski wrote:
| The entire concept of reactive objects brings back memories
| of extreme pain from KnockoutJS days (and terrible
| performance in complex scenarios to boot). I prefer just
| working with plain old data structures.
| benatkin wrote:
| I prefer working with just plain old setup functions,
| that are called once.
| simion314 wrote:
| >You forget one value in your dependency list and the
| weirdest things happen.
|
| 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.
| bobthepanda wrote:
| As far as I can tell you can just not use hooks, if you
| (and the rest of your contributors) agree.
| wayneftw wrote:
| > You forget one value in your dependency list and the
| weirdest things happen.
|
| Don't you get eslint warnings right in your editor?
|
| If you're using VS Code there's an extension for that...
| Waterluvian wrote:
| Not only this but I need the freedom to control the
| dependency list for an effect. Sometimes it isn't as simple
| as "every variable being referenced inside the effect."
| joshribakoff wrote:
| You can use OOP or whatever you want to build your
| abstraction and then surface that as a hook, rather than
| using hooks as a way to do all your logic. This is similar to
| how there are npm packages that just import some other js
| package "foo", wrap its api in a react component, and
| reexport it as react-foo
|
| 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.
| grandpoobah wrote:
| > You forget one value in your dependency list and the
| weirdest things happen
|
| 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.
| mikojan wrote:
| Seconded. Plus, you start writing wrapper functions for
| standard Javascript APIs (e. G. window.setInterval) to force
| them into Reacts data model.
| jameshart wrote:
| ... which is a good idea anyway because you want your code
| to be testable.
| mikojan wrote:
| ...this makes no sense at all.
| erikpukinskis wrote:
| Why would you do that? What's wrong with this?
| 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.
| lunfard00 wrote:
| in my experience is better to have customs hooks like
| useTimeout (react-use library is awesome) so is easier to
| test. Mocking hooks feels nice, mocking the browser API
| not so much...
| jimrandomh wrote:
| > You forget one value in your dependency list and the
| weirdest things happen.
|
| There's an eslint plugin specifically for detecting this,
| which should probably be considered mandatory.
| mrighele wrote:
| > 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?
|
| 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)
| jakelazaroff wrote:
| Note that you shouldn't (can't?) actually pass an async
| function directly to useEffect, since it returns a promise:
| https://github.com/facebook/react/issues/14326
| tanaypingalkar wrote:
| True, you cannot do that in useEffect, it will throw you
| error. And probably your linter will tell you. You can't
| even use your own or third party hooks in side useEffect.
| This also throw error I don't know why?
| mrighele wrote:
| you're right, good catch. Usually it's the TS compiler
| that prevents me doing that.
| zebnyc wrote:
| I usually use an IIFE to encapsulate the async block
| inside the useEffect. Works like a charm!
| jacobedawson wrote:
| Not true, see Gaeron's direct reply in the thread you
| linked to, that's been the standard for 3 years and works
| like a charm - useEffect specifically is the hook for
| handling async functions ("impure effects").
| azangru wrote:
| Notice the words "pass to useEffect" in the parent
| comment, and the code snippet in the grandparent they are
| commenting on. The type of the callback function that
| useEffect accepts is () => void rather than () =>
| Promise<unknown>. What's not true about that?
| jakelazaroff wrote:
| I reworded it to make it a bit more clear (at least, I
| thought so) -- "pass an async function to useEffect",
| meaning the argument supplied to useEffect itself, rather
| than "use directly within" which is ambiguous. Re-added
| "directly" to hopefully make even more clear. Sorry for
| the confusion!
| ricardobayes wrote:
| You can use a boolean ref for this in the useEffect to keep
| track whether it was the first render or not.
| https://reactjs.org/docs/hooks-faq.html#can-i-run-an-
| effect-...
| phailhaus wrote:
| > Debugging is painful. You forget one value in your
| dependency list and the weirdest things happen
|
| 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.
| schwartzworld wrote:
| Regarding useCallback, you can probably skip it in most
| cases, unless the extra renders are a problem for you.
|
| Linters should help with missing dependencies.
| ng12 wrote:
| Yes! useCallback is an optimization and using it
| prematurely can be problematic.
| antjanus wrote:
| that's pretty much how I feel, too. I really love the flow of
| hooks and I write a ton of them -- majority of the logic I
| write tend to end up in hooks and I really love that.
|
| There's something to having a function that can hold state or
| load its state on mount, etc.
| k__ wrote:
| They fit so well into React it's crazy.
|
| But they're also soooo idiosyncratic which makes them rather
| ugly on a general level.
| codeflo wrote:
| They also come with many restrictions and rules on correct
| and efficient usage, not all of them checked by ESLint, that
| make the learning curve a bit brutal.
|
| 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.
| radicalbyte wrote:
| Aren't there proper TypeScript bindings which catch (more)
| cases?
| wilsonrocks wrote:
| I'm excited to try this. I've tried xstate many times and always
| struggled with the typescript, if this Just Works I'll be v happy
| davidkpiano wrote:
| We're constantly working on improving the TypeScript experience
| with XState; there are some new things like `createModel()`
| that might help!
| cacozen wrote:
| Oh, I need to try that too!
| RobertKerans wrote:
| Actions defined in options are the killer for me at the
| minute (and guards, but actions are more important
| generally). I've had to resort to using enums (+ type guards
| + coercions) for everything. Have you any idea on if/when can
| get these in the model API? At the minute the API is nice,
| but not particularly useful with only context & events. The
| type gen library seems to be a sticking plaster more than
| anything else: it's clever, but building an NPM package on
| the fly seems super fiddly (and flat out doesn't work with
| newer Yarn setups), so movement forward on inference without
| it would be great
| wilsonrocks wrote:
| Oooh createmodel is new to me - will check it out!
| isuckatcoding wrote:
| So I think this is basically an abstraction on top of useReducer
| which itself is kind of a state machine pattern.
|
| It's a little verbose more my liking but I think it could be
| powerful for large apps with lots of states.
| vikingcaffiene wrote:
| This is a Finite State Machine meaning there are a limited set
| of states and state transitions allowed. useReducer doesn't
| have any of the state/transition validation logic which makes
| it an infinite state machine. Both are very useful. Different
| use cases though.
| runawaybottle wrote:
| Can anyone provide an example of a situation where something like
| this is a pragmatic solution? Please also provide the drop dead
| simple version too and explain why this would be more elegant and
| intuitive in comparison.
|
| 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.
| gbear0 wrote:
| I think the value of a state machine is treating all of the
| associated states in a single conceptual model or level. You
| can definitely do everything directly in a bunch of if
| statements and tracking state in separate variables, but that
| opens yourself to more potential bugs, higher maintenance cost,
| less big picture understanding, and even larger performance
| costs (separate codes means the compiler can't optimize as
| well; means hardware caches aren't as effective; there's more
| overhead to run separated functions; etc, etc).
|
| 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).
| runawaybottle wrote:
| I'll concede I came with a contentious tone, which is
| probably resulting in a similar retort by a few of you that
| is mostly using appeal to authority to justify your claims
| (eg just learn computer science bro, you must not know it).
|
| 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.
| davidkpiano wrote:
| Sorry if I was flippant. Here is an example of a minute
| timer app, which replicates Android's timer app behavior
| using statecharts: https://codesandbox.io/s/xstate-vue-
| minute-timer-viz-1txmk
|
| Allow popups to see a live visualization of the statechart.
| cacozen wrote:
| I think using a state machine can make the resulting code more
| elegant in some cases by coupling state changes and effects.
| But that's not why they are useful. In my experience, they have
| been useful because they help map (even mentally) all
| individual possible states a piece of code can be in, and how
| the code should behave in each case. In the process, I usually
| discover and deal with more edge cases, more combination of
| states and behavior, and as a consequence the code is
| bigger/fatter. But also more robust.
| enjeyw wrote:
| I've needed to construct a chatbot-esque UI on more than one
| occasion. I used a state-machine on the second time round and
| it simplified work considerably!
| davidkpiano wrote:
| Lots of good examples here: https://xstate-catalogue.com/
|
| 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/
| runawaybottle wrote:
| No, I know what state machines are. What I'm contesting is
| the argument that this is an intuitive way to structure code.
| It could be intuitive to reason about, or even white board
| the states your application can be in, but this doesn't look
| like concise and elegant code.
|
| 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.
| fuzzy2 wrote:
| I think your problem is that many things don't have to be a
| state machine. And you're right. It's simply not a good
| idea to create a confirmation dialog like that.
|
| 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.
| matsemann wrote:
| I actually believe there would be fewer UI bugs if more
| UI was modeled as a state machine. After all, all
| diagrams and design is often based on "when X is shown
| and Y is pressed, we go to Z". But in code that's
| expressed in a way that makes it possible to have a
| mismatch between multiple half baked states.
| davidkpiano wrote:
| Nobody's forcing you to use this library. Please take your
| negativity elsewhere.
|
| EDIT: Okay, didn't mean to interpret criticism as
| negativity; I apologize.
| runawaybottle wrote:
| Fair enough, but the echo chamber is why stuff like this
| gains prominence. I'm abrasive for sure, but I need to
| see some push back on these ideas in all of these
| threads.
| kaoD wrote:
| Criticism is not negativity. It's valuable when someone
| in the room challenges ideas and pushes back over-
| abstraction (if done respectfully and constructively).
| jhgb wrote:
| Can you even describe the use of a state machine for UIs
| as "over-abstraction"? To me it seems more like an
| "under-abstraction", since one usually compiles a more
| abstract definition into a state machine - often a
| regular expression or some kind of grammar.
| runawaybottle wrote:
| Was just discussing this with a friend this morning. It
| might be better to describe it in terms of trade-offs
| versus the overuse of the 'over-abstraction' allegation.
|
| 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.
| gbear0 wrote:
| Did you start programming with OOP or Functional
| programming? Did learning how to programming in the other
| form seem difficult to understand why you would do things
| that way, or seem obtuse vs just using the ways you
| already know?
|
| 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.
| jhgb wrote:
| How are state machines "a very different way of looking
| at code"? It's basically assembly with jumps as state
| transitions. A decent programmer already understands this
| from assembly lessons (and avoids doing it by hand,
| leaving it to the compiler).
| [deleted]
| jhgb wrote:
| > It seems like you're just unfamiliar with state machines.
|
| You're saying that he doesn't know computers? Computers are
| state machines. Presumably he used one to post his comment.
| RobertKerans wrote:
| Auth logic or similar. Somewhere area in app that may be
| genuinely complicated and can't go wrong, where you need
| granular control over a set of states which often have sub
| states, and where there are things like remote requests to
| different services occurring.
|
| 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
| runawaybottle wrote:
| That's actually a good use case, thanks for sharing.
| [deleted]
| skrebbel wrote:
| love the logo
| cacozen wrote:
| Hahaha, reused from a video on my YouTube channel, was created
| in 5 minutes on Apple Keynote
| yewenjie wrote:
| Is there quick primer on what and why of state machines?
| cacozen wrote:
| Author here. Yes, I made a series of videos about it, starting
| here: https://youtu.be/N0OaRdJuVlc
| whoevercares wrote:
| How does this work with RxJS or reactive pattern?
| cacozen wrote:
| It's not directly related - this is a thin layer on top of
| useReducer and useEffect, so it's more directly tied to React.
| But take a look at XState
| DylanSp wrote:
| Thanks for including the comparison to xstate; that's where my
| mind immediately went when I first saw this. Are there any plans
| to create a visualizer, similar to what xstate has?
| cacozen wrote:
| Of course, XState is a huge inspiration and the golden standard
| of State Machines for JS. I visualizer is not on the short-term
| plan, but there's a "verbose" mode that helps with debugging.
| lloydatkinson wrote:
| As someone that works with _finite_ state machines, I can't begin
| to describe how annoying I find it when everyone needlessly
| reduces the name down to just "state machine". To me, it makes it
| seem like the people making "state machines" don't understand the
| whole "finite" part and thus don't understand half the point.
| davidkpiano wrote:
| If the term "finite-state machine" were used instead, there
| would inevitably be people making comments like you, but
| instead complaining "this isn't finite, you have extra data in
| the machine that can be infinite". The pedantry isn't helpful.
| vikingcaffiene wrote:
| It's interesting to me how certain design patterns get
| rediscovered over the years. A few years back FSM's were a dirty
| word for crusty Java devs. I myself avoided them for that reason.
| Then one day I had a perfect use case for one and took the plunge
| and was amazed by how useful a pattern it is. I'm embarrassed at
| how long it took me to get hip to it. I'm writing a feature at my
| job right now that makes heavy use of FSMs. Very testable and the
| nature of the pattern means you can have a lot of confidence in
| your state being valid. I highly recommend trying it out even if
| you don't adopt this hook or xstate (you can make a FSM fairly
| easily)
| codemonkey-zeta wrote:
| Finite state machines will never _not_ be useful for the same
| reason regular expressions will always be useful. They 're a
| supremely simple model of computation, making them easy to
| reason about.
|
| The problem with state machines in Java is not the state
| machine's fault, it's Java's fault.
| vikingcaffiene wrote:
| I feel like you are refuting a point I didn't make. I was
| just talking about how we re-discover patterns. My feelings
| on Java have nothing to do with it.
___________________________________________________________________
(page generated 2021-05-22 23:01 UTC)