Redux was shit. It made apps less comprehensible and less performant.
Immutable state isn't really a great idea for stateful applications, and UI development is an almost purely stateful activity. It can easily be said that any UI is fundamentally a state machine...the state of your UI defines the set of actions that are available to it, and the actions taken define the future state of the UI. This is not just true for web UIs, but all UIs. It is just as true for your blender or transmission lever as it is for your react app. And being a stateful problem, you're far more likely to have to change state than you'll have to change how that state is rendered. This puts redux-style solutions squarely on the wrong side of the expression problem. And the fact that your state is essentially a global variable, using it means you spend vast amounts of time trying to thread constantly changing data types through intricately nested pure functions, and it just fucking sucks.
I'm generally a pure functional programming kind of nerd, but when it comes to difficult inherently stateful programming problems like UI dev, game programming, or simulations, nothing is better than plain old encapsulated state via classes (or maybe actors). Anybody who advocates for pure functional approaches to these types of problems has lost the plot.
> It can easily be said that any UI is fundamentally a state machine
This doesn't make sense to me as a criticism of Redux. Redux is a giant state machine. That's all it is. Actions are transitions and the store is the current state. The state transition table is the reducer.
The Redux architecture (and the Elm architecture that inspired it) is about the purest expression of a state machine you're likely to find in any architecture. The fact that a UI is just a giant state machine is the whole reason these architectures were created in the first place.
Redux is tricky and boilerplate-y because of some interactions with React, because it doesn't come with a built-in mechanism for handling interactions with the world "outside" the UI (e.g. interactions with the backend), and because JS doesn't have facilities for immutability out of the box and rather you must bolt on post-hoc solutions.
1. The datastore is a massive global. This already brings to it many of the pitfalls that have caused programmers everywhere to avoid global variables like the plague. But in addition to that, for incredibly complex apps, it is a massive burden to maintain all of your state in one place, without any ability to encapsulate or localize trivial local states. Why do I need a global state tree to determine which option is selected in my select dropdown? Why should I bundle the varying hyperlocalized implications of onChange actions vs onSelect actions with the business implications of things like payment submission states? Why does the state of one button for basic user A have to be bundled in the same data structure as an input element three tabs over, two levels deeper, meant for admin user B with privileged access?
2. Robbed of the concept of benign localized state, all state transitions must be weaved through the entire complex functional tree that renders it. Small refactors of your UI, like moving a widget from one place to a different location in your application "tree", involve not just refactoring the component which renders the widget, but every single component that it passes through, to reroute the data to the right location. It is extremely high touch refactoring, exacerbated by javascript's dynamic nature which is already hard to refactor, and Redux's inability to play nice with static typing tools like Typescript.
Redux's bad ideas do not come from the fact that it models a state machine, it comes from the fact that it models it poorly and forces you into using a giant state machine, intricately threaded and tightly coupled throughout your entire application, when smaller state machines are easier to understand and easier to compose.
Luckily, there is another programming construct that allows you to easily model state machines, and allows you to build them arbitrarily large or small to meet your demands, and allows you to encapsulate data and actions extremely well so that state and associated actions do not leak. They're called classes.
Note that we have _always_ advised that you should avoid putting literally every piece of state into the Redux store, and we do _strongly_ encourage keeping local state in components:
Which goes to the point that redux is not perfect and has its own downsides. I generally take the simplest solution to solving a problem and favor readability over abstraction whenever possible.
A single global state machine and a bunch of small state machines can be losslessly transformed from one to the other. And indeed Redux offers tooling to transform from one to the other depending on preference (this is the Redux Toolkit).
And indeed in that world they don't have to be bundled together. You can have separate state machines for different parts of the page.
> Robbed of the concept of benign localized state, all state transitions must be weaved through the entire complex functional tree that renders it.
As you say, this is really just a specialization of 1 right rather than a separate problem?
> They're called classes.
Classes of course can model state machines since any Turing complete style of programming can model state machines. However, they are significantly different from the classic representation of state machines in that the important part of representing state machines is that you can inspect their state. Classes are meant to be opaque; as you say their encapsulation is a feature.
But their opaqueness prevents the usual state machine composition, which is to link different state machines together based on the states they are currently in. Whether or not that's a good thing depends on the circumstances, but they make it painful when you really do want to represent things as a state machine proper, and not simply as a stateful component. And to the extent that you want a UI to be a state machine, that's a bad thing.
Of course not all parts of your UI should be a state machine, especially things that involve continuous states rather than discrete ones. For truly mind-numbingly benign stateful details (such as which frame a button should be in while performing an animated transition from one color to another) you can just put it directly in the React component and ignore the store altogether, and that's definitely where you really don't want explicit state machines, but rather encapsulated black boxes that are stateless from the perspective of the overall system (which is effectively what a component-based/class-based system looks like when interacting with a state machine).
> I'm sure there are patterns that could help with this.
Indeed there are. Again any Turing complete language can do anything another language can with a sufficient dose of design patterns (in the limit design patterns just recreate another language). So everything I say about "classes" and "state machines" needs to be taken as talking about level of effort not possible vs. impossible, since the former, being Turing complete in most cases, can always model the latter.
But to return to the meat of your questions:
> Why? Is that because you need to know if a transition is valid for a given state?... Not sure what you're refering to by this.
In a manner of speaking yes this is about validity of transitions, but it only shows up when you're making a bigger state machine from smaller ones.
What I meant by "state machine composition" and "important part of representing state machines is that you can inspect their state" relates to the combinatorial explosion of states that occurs when you build a larger state machine from smaller state machines.
When you combine state machines naively you get a new state machine that is the combination of all its child states (the n-tuple of all the child states). These usually (but not always) are not all valid states at the parent state level. That is usually, at the parent state level, only some subset of the combination of every possible child state is a valid overall state.
This means, if you're thinking of things purely as state machines all the way down, you don't want your child state machines to be black boxes. Instead, you want to be able to form relationships between the different child states to be able to constrain them in different ways. In a statically typed language with algebraic data types this might show up as various different sum types that are subsets of tuples. In a language without sum types this often ends up being basically the visitor pattern. In a dynamically typed language this might show up as various assertions about the overall n-tuple. Either way, you need some way to "open up" the child states to inspection by the parent state.
Now sometimes, your parent state truly is the n-tuple of all its child states, i.e. the child state can be any state at all and this is still a valid parent state. At the UI level, this is usually true for anything a user would be unlikely to care about if the page were to refresh and that state was reset, for example the state that governs the animation of a flashing button, or a drop-down menu being open or closed. This is what I meant when I said "black boxes that are stateless from the perspective of the overall system." From the parent system's perspective, if the child component is a black box, you could substitute that child component with a completely stateless placeholder (e.g. a static button) and the overall system would not be in an incorrect state.
And it is precisely these places where it is most convenient to use opaque child states to model things.
However, most "significant" things in a UI are things where we do care about constraining the n-tuple of child states to a subset. We often want certain parts of the UI to gate other parts of a UI (e.g. greying out certain parts of a UI), or want certain actions in one part of the UI to cascade in a specific way across the rest of the UI.
Don't get me wrong, getting back to the beginning of my reply, I'm by no means saying that this is impossible with opaque classes. After all there have been people making perfectly functional UIs with opaque classes for a long long time. It's merely more annoying and bug-prone.
If all your child states are opaque, then all this subsetting of valid states is implicit in control flow rather than explicit in data definitions. And as Fred Brooks put it: "Show me your flowchart [control flow] and conceal your tables [data definitions], and I shall continue to be mystified. Show me your tables, and I won't usually need your flowchart; it'll be obvious."
I'm not sure what you would want from a citation? Somebody other than me saying that the important part of representing state machines is being able to inspect their state? Would you accept prior art? Languages that are explicitly built to model state machines, such as TLA+, generally expose the state explicitly by default and build all their reasoning facilities around manipulating the state directly (TLA+ is indeed generally not an executable language, but there aren't many executable languages I can think of at the moment that really go all-in on making everything a state machine like TLA+ does and so aren't great illustrations of what pure state machines are like; Elm does this as well, but Elm and Redux are the same core approach so it doesn't really count in a discussion about Redux).
> Redux is the purest expression of a state machine possible
Are you sure "pure" is the right word? Last I checked redux very proudly incorporates a certain event-sourcing something idea in it. It's definitely not a "just state machine" library. More like in KFC when you order something, they always make sure that you get their sugar water as well.
What do you mean by "a certain event-sourcing something idea?"
I really do mean "pure." That doesn't mean that Redux doesn't provide other facilities on top, such as React interop, error-handling, logging, etc. But if you don't need any of that and you use the core of Redux, it really is just a state machine representation.
Let's write out the classic finite state machine representation of a locked/unlocked item.
States: Locked, Unlocked
State transition table:
Current State | Transition | Next State
---------------------------------------
Locked | Lock | Locked
Unlocked | Lock | Locked
Locked | Unlock | Unlocked
Unlocked | Unlock | Unlocked
Initial State: Unlocked
That's a textbook definition of an FSM.
This is the equivalent Redux code.
import { createStore } from 'redux';
const initialState = 'Unlocked';
const reducer = (currentState, transition) => {
if (currentState === 'Locked' && transition === 'Lock') {
return 'Locked';
} else if (currentState === 'Locked' && transition === 'Unlock') {
return 'Unlocked';
} else if (currentState === 'Unlocked' && transition === 'Lock') {
return 'Locked';
} else if (currentState === 'Unlocked' && transition === 'Unlock') {
return 'Unlocked';
}
};
const store = createStore(reducer, initialState);
// We're done! Now we can play with our state machine
console.log(store.getState()); // 'Unlocked'
store.dispatch('Lock');
console.log(store.getState()); // 'Locked'
store.dispatch('Lock');
console.log(store.getState()); // 'Locked'
store.dispatch('Unlock');
console.log(store.getState()); // 'Unlocked'
That's it. That's Redux. This is exactly a 1-to-1 translation of the FSM. I don't see how it could be any more direct (at least in JS). Everything else in Redux is optional.
If you think that's the simplest you could get, more power to you. I personally think your example is convoluted for such a simple example, but that's just me.
However, I should point out something that is not actually trivial about your code, by proposing a refactor: your FSM adequately models a car lock button interface, but not the lock itself. If we are modeling the lock, you have two invalid transitions...because it is impossible to lock an already-locked lock, and it is impossible to unlock an already-unlocked lock.
Classes actually shine quite well here.
class On {
constructor(){
console.log('turned on')
}
turnOff(){
console.log('turning off...')
return new Off();
}
}
class Off {
constructor(){
console.log('turned off')
}
turnOn(){
console.log('turning on...')
return new On();
}
}
const toggle = new On();
toggle.turnOff().turnOn().turnOff().turnOff()
// ^ cool, ^ cool, ^ cool. ^ oh shit, typescript really doesn't want to let me do this.
Classes can model your original scenario quite easily (just add a `turnOn()` method to `On` and `turnOff()` to `Off`, but redux can't model my scenario (at least not without more boilerplate and lots of guards). More importantly, the available actions are localized...I don't have to create a monolithic state transition table logic...I only have to concern myself with the possible transitions for any given state. This makes it trivial to add new states or new actions, because I will never have to worry about the combinatorial explosion that can happen in a global transition table.
Let's step back. I think your approach work really well if you model state machine for internal usage.
However, Redux is meant to be for UI work, which is a side effect. You cannot control side-effect. You cannot guarantee that user will not try to turn switch off twice.
Even in idiomatic Redux, the switch case for reducer always include `default:` which mean everything else go here.
As you said, the Redux FSM models a car lock button interface which is exactly what Redux try model.
> I personally think your example is convoluted for such a simple example, but that's just me.
It is indeed all a matter of taste in the end, but I mean it's the exact transcription of a textbook FSM. I don't think your example would get that much simpler with a stable identifier, which brings me to:
> I don't have to create a monolithic state transition table logic
It doesn't have to be monolithic. I just created a monolithic function because it's easier for this small example. It could just as easily dispatch on state (which is what you're doing here), or on transition (which is impossible with your hierarchy), or mix and match them. I can write those examples if you're curious.
Your class example is a good example of a state machine trace (e.g. one that you might often use to create an ephemeral config object), but it's not an example of an actual persistent state machine. As soon as you use a stable identifier the type safety goes out the window.
let stateMachine = new On();
// Wait a bit for the user to do something
stateMachine = statemachine.turnOff();
// Wait again for the user to do something
stateMachine = stateMachine.turnOn();
Depending on how you annotate the initial let, one of those two lines will cause TS to complain (or you have to do a manual cast somewhere which circumvents the type safety).
Indeed if you just care about a state machine trace the same type safety holds in the explicit state transition table case if you just use TS's literal types in the reducer's type annotations. You just explicitly call the reducers you need however many times and then persist the state machine at the very end.
That is your type safety doesn't come from the representation as an object, but rather that there is no stable state machine between invocations, but only an ephemeral trace of one whose intermediate states are immediately destroyed. Traces are valuable! But you could generate the exact same trace with an explicit state transition function and you would get the exact same type safety that way. I can write it out for you if you're curious.
> I will never have to worry about the combinatorial explosion that can happen in a global transition table.
Again you don't have to worry about a combinatorial explosion in a global transition table either if you dispatch on actions or states.
The fundamental difference between what you've outlined here and the explicit state transition table is that in your approach states are first-class and transitions are not (but rather methods attached to states), whereas the explicit state transition function treats both of them as first-class entities.
For places where you really only need an ephemeral state machine trace, rather than a state machine that persists between calls, you don't need first-class transitions because the transitions are all ephemeral and cannot be dynamically called at runtime.
Where you do have users able to dynamically call transitions at runtime, you end up needing to represent those transitions somehow, and you end with something approaching Redux (indeed I think an interesting exercise would be to implement On and Off where the user either presses "a" or "b" at the keyboard to turn on and off with your classes. I suspect you end up with just the Redux approach all over again).
EDIT: I want to emphasize I don't think Redux is free of faults. There are many things I really dislike about it and the React ecosystem it integrates into. But I don't think any of that can be chalked up to a poor representation of a state machine.
As do the reducers. For example the following is a valid type signature.
function onToOff(initialState: "on", transition: "toggle"): "off"
In each of the if clauses those are what the types are inferred as, exactly equivalent in type safety to the classes (and more flexible because you can dispatch on action).
I chose string for simplicity (string literals happen to be distinct types on their own, I could easily use anything else other than strings, e.g. interfaces). Heck it could just be integers and be even simpler.
Look every of your comment is really tiring to read, so fucking long with little substance like redux code. Everyone already knows what redux is, it's really garbage and leaky. For one there is a dispatch function, where is this in the classic "pure" state machine? Why can't state transition be just a function call? Secondly there is no type that you can infer from this, you have to "write typescript" by adding type annotations like an idiot. Or else you could dispatch anything because again there need to be a dispatch function because reason.
The other commenter gave example of implementation as type safe immutable builder. You can even simplify their implementation further by using plain functions instead of classes.
let's back off a bit. I agree with the other commenter's point, which is the really important thing:
> Redux's bad ideas do not come from the fact that it models a state machine, it comes from the fact that it models it poorly
So to me it doesn't really matter whether class is the best tool to implement state machine.
If the argument is "you can do it with class/function/language feature x, however it's a lot of work to achieve composition or whatever", then the same can be said about redux:
it kind of looks like a state machine, but in order to have composability/validation/static types etc, you still have to apply a bunch of "patterns". So it's just as effective (or ineffective) as the approach of rolling your own state machine.
But that's precisely where I strongly disagree. Redux's problems do not come from a poor implementation of a state machine, but rather the interactions between state machines and stateful, non-state machine representations.
Again, Redux is the purest expression of a state machine possible in a general-purpose programming language (as opposed to a specialized one like TLA+). It maps one-to-one with the usual textbook definition (technically textbooks don't have a rigorous definition for state machines, but rather finite state machines, but usually by "state machine" we just mean you take a normal FSM and just relax the finite bit by introducing some infinite component into the overall state such as an arbitrarily long list). The store is the state, the actions are transitions, and the reducer is the state transition table.
Redux's infelicities come precisely from the fact that it needs to interact with things that are not modeled as state machines! Namely, interacting with black box children and the outside world (mostly sending stuff to the outside world, Redux comes with stuff out of the box to deal with just one-way inputs from the world). And Redux doesn't come with ways out of the box to deal with those so you need to layer more stuff to make it all play nicely together. But you need at some point to interact with them to work with the JS ecosystem so you need that stuff.
If you had state machines all the way down you don't need any particular patterns other than the fundamental state machine description! Especially with Typescript's structural types, everything just works (you just pull out chunks of your store into little stores and pull chunks of your big reducer out into little reducers, it's literally just cut-and-pasting chunks of your code and giving it a new name and making new state machines all the way down).
The friction comes from interacting with React, dealing with the outside world, and figuring out various decisions of what to do about data structures that the JS stdlib doesn't decide for you out of the box (e.g. what immutable library should one use). Even the choice of mutable vs immutable is something that is decoupled from Redux proper (it turns out for incidental reasons it's easier for type systems to properly scope local effects with immutable data structures than mutable ones, but that's more an artifact of Typescript than Redux, see e.g. Rust's type system for a counterexample).
If all of that was just more state machines (and the stdlib issues were taken care of) then Redux would be absolutely lovely.
And that's not just a pie-in-the-sky hypothetical. E.g. Elm is basically what Redux would be if everything was state machines and works great in all the places Redux doesn't (Elm's infelicities in turn are of a different variety that stem from some limitations of its type system to properly express some of the structural types you'd like state machines to have, in particular a lack of structural union types that often requires duplicating the state transition table in certain ways as you break a larger state machine down into smaller state machines).
I think you’re illustrating one of redux’s real downfalls: it can be used to hold the entire state for your UI, even though it shouldn’t. All your complaints are solved when adhering to the official recommendations. Maybe you got into redux early or worked on a codebase that took things too far.
A global state object is one of the nicer features when dealing with redux.
> you're far more likely to have to change state than you'll have to change how that state is rendered. This puts redux-style solutions squarely on the wrong side of the expression problem
Maybe, it's a bit hard for me to describe succinctly.
Your UI as an application likely has dozens, if not hundreds or thousands of potential states. And the more you develop the feature set of your UI, the more states you will introduce to it. The data that represents these various states is going to constantly evolve to be able to represent them: you'll have evolving business state, evolving widget states, evolving URL states, etc.
The expression problem can be summarized as a practical comparison of two types of extensibility: writing functions, which extend through composition, and writing classes, which extend through inheritance. The choice of which one is better starts with asking what is more likely to change: your data or your computation over that data.
If you have a problem where your data types are relatively static, but the computations you do on that data are constantly changing, pure functional programming is where it is at. You just write different functions to do different things, and then compose them together to do what you want. This sort of processing is perfect for a ton of usecases...things like data pipelines and etl, machine learning, web services, etc.
If you have a problem where your data types are constantly changing but your computation is relatively static, you're going to have a very bad time with a pure functional approach. You'll end up having to edit multiple functions just to change a single data type. When you decide that you no longer want to represent your state with an Array<number> and instead want to represent it with a Set<CustomObject>, you're gonna have to refactor every single function that the original version passed through. And with a massive global state object like Redux, that means not just refactoring the component that renders it, but potentially every single component above it in the HTML tree, modifying them so that they can pass it through to its final destination. That is the sort of burden that happens when you fall on the wrong side of the expression problem.
Immutable state isn't really a great idea for stateful applications, and UI development is an almost purely stateful activity. It can easily be said that any UI is fundamentally a state machine...the state of your UI defines the set of actions that are available to it, and the actions taken define the future state of the UI. This is not just true for web UIs, but all UIs. It is just as true for your blender or transmission lever as it is for your react app. And being a stateful problem, you're far more likely to have to change state than you'll have to change how that state is rendered. This puts redux-style solutions squarely on the wrong side of the expression problem. And the fact that your state is essentially a global variable, using it means you spend vast amounts of time trying to thread constantly changing data types through intricately nested pure functions, and it just fucking sucks.
I'm generally a pure functional programming kind of nerd, but when it comes to difficult inherently stateful programming problems like UI dev, game programming, or simulations, nothing is better than plain old encapsulated state via classes (or maybe actors). Anybody who advocates for pure functional approaches to these types of problems has lost the plot.