3 ms·
I'm definitely a fan of the concept of using state machines for UI development, but both of the libraries I've tried so far, XState and Robot (or at least their
by lewisl9029 6y ago
I'm definitely a fan of the concept of using state machines for UI development, but both of the libraries I've tried so far, XState and Robot (or at least their official React bindings), seem to have a bit of an impedance mismatch when used with React:
Both of these libraries' React integration involves creating a "service" that accepts events and transitions between states internally, bypassing React's own state management lifecycle, which is tightly integrated with rendering. In the past I've seen a number of nasty bugs pop up when precise synchronization between theses two sources of truth for state is required, and they can be a bit of a nightmare to debug and/or work around.
I think a better, more robust integration between state machine implementations and view layers like React with their own mechanisms for state management could potentially involve getting rid of the "service" middle man, and instead delegating internal state transitions of the machine at runtime to React itself, using a mechanism like the dispatch function from useReducer, which seems like a perfect fit.
I've been itching to explore this concept a bit more deeply but haven't found the time or motivation between work and various side projects. Would love to see other folks pick up and run with it if they're interested though!
- aphextron 6y ago>Both of these libraries' React integration involves creating a "service" that accepts events and transitions between states internally, bypassing React's own state management lifecycle, which is tightly integrated with rendering. In the past I've seen a number of nasty bugs pop up when precise synchronization between theses two sources of truth for state is required, and they can be a bit of a nightmare to debug and/or work around. I've been using XState and React on a fairly sophisticated large scale application for the last two years. The key is using React hooks with pure components, and eschewing the lifecycle altogether. This turns React basically into just a highly efficient view rendering library, while delegating all of your application logic to the state machine. Your components end up way cleaner and easier to reason about when they are just pure functions without side effects, and all your business logic is contained within a state machine.
- lewisl9029 6y agoI think that's certainly a valid approach, and comes with a ton of benefits. But I don't think that has to be the _only_ viable approach to use a state machine in React without having to worry about subtle synchronization bugs. Especially since even if our endgame is to go all-in on the state machine as our only source of truth for state, we could still benefit from a smooth transition path to get there. Using state machines deliberately for complex flows while delegating simpler state management tasks to simple useState hooks should ideally be just as robust and well supported as modeling our entire UI as a giant state machine. At the end of the day, the state machine definition in both of these libraries is just a bunch of data, to be interpreted by some runtime that's responsible for maintaining internal state and reacting to events. Currently that runtime is a generic stateful "service" implementation provided by these libraries, but there's no reason why that runtime couldn't be some stateful service implementation built on top of React, or any other view layer with its own opinions on state management.
- adamkl 6y ago”Currently that runtime is a generic stateful "service" implementation provided by these libraries” This is how I approached using XState in a proof of concept last year. I created a “service layer” made up of XState machines that handled different high-level aspects of the application behaviour; i.e. not tied specifically to a particular UI element. Things like managing the requests made during an OAuth flow and storing the authenticated user’s information. This gave me a source of application state with well defined behaviours that I could use instead of “redux” and its usual accompaniment of boolean flags and thunks. The best part of embracing the separation of state management from React is that I initially developed the application in Angular before lifting-and-shifting the “service layer” over to React and sticking in a “context”. All the application behaviours remained the same, it was just a matter wiring up some JSX in place of Angular’s HTML templates.
- tunesmith 6y ago> The key is using React hooks with pure components, and eschewing the lifecycle altogether. Can someone explain this in a little more detail? Particularly the eschewing the lifecycle altogether. Does this basically mean delaying the view render until all the business logic is evaluated, aiming for only one render per operation?
- the_gipsy 6y agoI think Elm addresses this issue in a way. It has some sort of "update/render" cycle baked in, with strong typing and sum types.
- steve_adams_86 6y agoI can’t share the code, but this is exactly what we do on my team. Using typescript and a light wrapper around useReducer, you can create a fairly robust state machine hook which uses a fairly simple interface to represent various valid states and how they can transition safely. You’re right in thinking keeping state management in react is an asset. It’s where the state should always be, in my opinion. React is excellent at managing state and the more ways you can avoid doing it elsewhere, the happier you’ll be. I’ll see if I can pull some sample code out without violating any contracts. I had a lot of fun putting the hook together - it was the project that totally sold hooks for me. They’re remarkably powerful due to ease of compositions and predictability of their state management. Combined with typescript it has been a total game changer for me.
- jameshart 6y agoAgreed. I suspect this example would have some surprising behaviors if the component orchestrating the confirmation flow were rerendered while the confirm dialog is being displayed. Does the state machine get reinitialized, closing the dialog? Or is the ‘confirming’ state retained, but the onConfirm callback in the context updated? Or is the ‘in flight’ state machine retained? In any case, is that represented by a state transition or is the state machine just replaced with a new one? A state system more tightly connected to react’s own model would allow it to address those concerns more directly.
- kn8 6y agoFor what it's worth, I've been working on a state machine library that's more deeply integrated into React: https://github.com/humaans/react-machine https://github.com/humaans/react-machine. It does a few things differently from XState / Robot bindings: 1. Use React's useReducer for state transitions and useEffect for applying the effects. 2. Allow reacting to component props (or machine context) and transitioning within the same render where props changed (achieved with a dispatch in the render body). This means the component's children get rendered with props and machine state that is in sync. 3. Separate machine's context (often component props) from machine's state. This reduces the number of unnecessary rerenders, while allowing your machine's reducers and effects to use up to date data and functions passed via props. All in all, it's my exploration of how to more tightly integrate statecharts into React's render lifecycle and programming model.
- lewisl9029 6y agoThis looks almost exactly like what I was hoping for! Thank you for sharing! Love the fact that you've settled on mimicking Robot's one-way-to-do-things functional composition API (that's my preference as well), and have plans for maintaining compatibility with XState's ecosystem for visualizations (that's imo XState's killer feature over Robot). This really looks like it has the potential to offer the best of both XState and Robot _and_ offer a more seamless React integration! Will definitely be giving it a shot and keeping a close eye on future developments.
- blktiger 6y agoAs far as I understand (though I could be mistaken) the xstate hooks ensure that the component only re-renders when the machine has "settled" after a transition. I don't see how the dispatch function from useReducer is any different. Both useReducer/dispatch and useMachine/send are sending an event that is then handled before triggering a rerender.
- lewisl9029 6y agoIME, what ends up causing issues is the fact that the component calling useMachine can rerender with new state/props that the machine may depend on, causing the machine to sometimes have an outdated view of the world, since its own internal state management (for things like the "state" itself as well as context, guards, other functions) is not fully synchronized with React.
- blktiger 6y agoSure but you have the same problem with useReducer too. If that is a problem then you should be pulling the call to useMachine up to an appropriate level then passing the service down for useService (in xstate) so that changes to the component don't impact the useMachine call.