DEV Community
Redux is fantastic.
Some of you might disagree, so let me tell you why.
Over the last few years, Redux has popularized the idea of using message-passing (also known as event-driven programming) to manage application state. Instead of making arbitrary method calls to various class instances or mutating data structures, we now can think of state as being in a "predictable container" that only changes as a reaction to these "events".
This simple idea and implementation is universal enough to be used with any framework (or no framework at all), and has inspired libraries for other popular frameworks such as:
However, Redux has recently come under scrutiny by some prominent developers in the web community:
If you don't know these developers, they are the co-creators of Redux themselves. So why have Dan and Andrew, and many other developers, all but forsaken the use of Redux in applications?
The ideas and patterns in Redux appear sound and reasonable, and Redux is still used in many large-scale production apps today. However, it forces a certain architecture in your application:
As it turns out, this kind of single-atom immutable architecture is not natural nor does it represent how any software application works (nor should work) in the real-world.
Redux is an alternative implementation of Facebook's Flux "pattern". Many sticking points and hardships with Facebook's implementation have led developers to seek out alternative, nicer, more developer-friendly APIs such as Redux, Alt, Reflux, Flummox, and many more.. Redux emerged as a clear winner, and it is stated that Redux combines the ideas from:
However, not even the Elm architecture is a standalone architecture/pattern, as it is based on fundamental patterns, whether developers know it or not:
Rather than someone inventing it, early Elm programmers kept discovering the same basic patterns in their code. It was kind of spooky to see people ending up with well-architected code without planning ahead!
In this post, I will highlight some of the reasons that Redux is not a standalone pattern by comparing it to a fundamental, well-established pattern: the finite state machine. This is not an arbitrary choice; every single application that we write is basically a state machine, whether we know it or not. The difference is that the state machines we write are implicitly defined.
I hope that some of these comparisons and differences will help you realize how some of the common pain points in Redux-driven applications materialize, and how you can use this existing pattern to help you craft a better state management architecture, whether you're using Redux, another library, or no library at all.
What is a finite state machine?
(Taken from another article I wrote, The FaceTime Bug and the Dangers of Implicit State Machines):
Wikipedia has a useful but technical description on what a finite state machine is. In essence, a finite state machine is a computational model centered around states, events, and transitions between states. To make it simpler, think of it this way:
- Any software you make can be described in a finite number of states (e.g.,
idle
,loading
,success
,error
) - You can only be in one of those states at any given time (e.g., you can’t be in the
success
anderror
states at the same time) - You always start at some initial state (e.g.,
idle
) - You move from state to state, or transition, based on events (e.g., from the
idle
state, when theLOAD
event occurs, you immediately transition to theloading
state)
It’s like the software that you’re used to writing, but with more explicit rules. You might have been used to writing isLoading
or isSuccess
as Boolean flags before, but state machines make it so that you’re not allowed to have isLoading === true && isSuccess === true
at the same time.
It also makes it visually clear that event handlers can only do one main thing: forward their events to a state machine. They’re not allowed to “escape” the state machine and execute business logic, just like real-world physical devices: buttons on calculators or ATMs don’t actually do operations or execute actions; rather, they send "signals" to some central unit that manages (or orchestrates) state, and that unit decides what should happen when it receives that "signal".
What about state that is not finite?
With state machines, especially UML state machines (a.k.a. statecharts), "state" refers to something different than the data that doesn't fit neatly into finite states, but both "state" and what's known as "extended state" work together.
For example, let's consider water 🚰. It can fit into one of four phases, and we consider these the states of water:
liquid
-
solid
(e.g., ice, frost) -
gas
(e.g., vapor, steam) plasma
Water phase UML state machine diagram from uml-diagrams.com
However, the temperature of water is a continuous measurement, not a discrete one, and it can't be represented in a finite way. Despite this, water temperature can be represented alongside the finite state of water, e.g.:
-
liquid
wheretemperature === 90
(celsius) -
solid
wheretemperature === -5
-
gas
wheretemperature === 500
There's many ways to represent the combination of finite and extended state in your application. For the water example, I would personally call the finite state value
(as in the "finite state value") and the extended state context
(as in "contextual data"):
const waterState = {
value: 'liquid', // finite state
context: { // extended state
temperature: 90
}
}
But you're free to represent it in other ways:
const waterState = {
phase: 'liquid', // finite state
data: { // extended state
temperature: 90
}
}
// or...
const waterState = {
status: 'liquid', // finite state
temperature: 90 // anything not 'status' is extended state
}
The key point is that there is a clear distinction between finite and extended state, and there is logic that prevents the application from reaching an impossible state, e.g.:
const waterState = {
isLiquid: true,
isGas: true, // 🚱 Water can't be both liquid and gas simultaneously!
temperature: -50 // ❄️ This is ice!! What's going on??
}
And we can extend these examples to realistic code, such as changing this:
const userState = {
isLoading: true,
isSuccess: false,
user: null,
error: null
}
To something like this:
const userState = {
status: 'loading', // or 'idle' or 'error' or 'success'
user: null,
error: null
}
This prevents impossible states like userState.isLoading === true
and userState.isSuccess === true
happening simultaneously.
How does Redux compare to a finite state machine?
The reason I'm comparing Redux to a state machine is because, from a birds-eye view, their state management models look pretty similar. For Redux:
state
+action
=newState
For state machines:
state
+event
=newState
+effects
In code, these can even be represented the same way, by using a reducer:
function userReducer(state, event) {
// Return the next state, which is
// determined based on the current `state`
// and the received `event` object
// This nextState may contain a "finite"
// state value, as well as "extended"
// state values.
// It may also contain side-effects
// to be executed by some interpreter.
return nextState;
}
There are already some subtle differences, such as "action" vs. "event" or how extended state machines model side-effects (they do). Dan Abramov even recognizes some of the differences:
A reducer can be used to implement a finite state machine, but most reducers are not modeled as finite state machines. Let's change that by learning some of the differences between Redux and state machines.
Difference: finite & extended states
Typically, a Redux reducer's state will not make a clear distinction between "finite" and "extended" states, as previously mentioned above. This is an important concept in state machines: an application is always in exactly one of a finite number of "states", and the rest of its data is represented as its extended state.
Finite states can be introduced to a reducer by making an explicit property that represents exactly one of the many possible states:
const initialUserState = {
status: 'idle', // explicit finite state
user: null,
error: null
}
What's great about this is that, if you're using TypeScript, you can take advantage of using discriminated unions to make impossible states impossible:
interface User {
name: string;
avatar: string;
}
type UserState =
| { status: 'idle'; user: null; error: null; }
| { status: 'loading'; user: null; error: null; }
| { status: 'success'; user: User; error: null; }
| { status: 'failure'; user: null; error: string; }
Difference: events vs. actions
In state machine terminology, an "action" is a side-effect that occurs as the result of a transition:
When an event instance is dispatched, the state machine responds by performing actions, such as changing a variable, performing I/O, invoking a function, generating another event instance, or changing to another state.
This isn't the only reason that using the term "action" to describe something that causes a state transition is confusing; "action" also suggests something that needs to be done (i.e., a command), rather than something that just happened (i.e., an event).
So keep the following terminology in mind when we talk about state machines:
- An event describes something that occurred. Events trigger state transitions.
- An action describes a side-effect that should occur as a response to a state transition.
The Redux style guide also directly suggests modeling actions as events:
However, we recommend trying to treat actions more as "describing events that occurred", rather than "setters". Treating actions as "events" generally leads to more meaningful action names, fewer total actions being dispatched, and a more meaningful action log history.
Source: Redux style guide: Model actions as events, not setters
When the word "event" is used in this article, that has the same meaning as a conventional Redux action object. For side-effects, the word "effect" will be used.
Difference: explicit transitions
Another fundamental part of how state machines work are transitions. A transition describes how one finite state transitions to another finite state due to an event. This can be represented using boxes and arrows:
This diagram makes it clear that it's impossible to transition directly from, e.g., idle
to success
or from success
to error
. There are clear sequences of events that need to occur to transition from one state to another.
However, the way that developers tend to model reducers is by determining the next state solely on the received event:
function userReducer(state, event) {
switch (event.type) {
case 'FETCH':
// go to some 'loading' state
case 'RESOLVE':
// go to some 'success' state
case 'REJECT':
// go to some 'error' state
default:
return state;
}
}
The problem with managing state this way is that it does not prevent impossible transitions. Have you ever seen a screen that briefly displays an error, and then shows some success view? If you haven't, browse Reddit, and do the following steps:
- Search for anything.
- Click on the "Posts" tab while the search is happening.
- Say "aha!" and wait a couple seconds.
In step 3, you'll probably see something like this (visible at the time of publishing this article):
After a couple seconds, this unexpected view will disappear and you will finally see search results. This bug has been present for a while, and even though it's innocuous, it's not the best user experience, and it can definitely be considered faulty logic.
However it is implemented (Reddit does use Redux...), something is definitely wrong: an impossible state transition happened. It makes absolutely no sense to transition directly from the "error" view to the "success" view, and in this case, the user shouldn't see an "error" view anyway because it's not an error; it's still loading!
You might be looking through your existing Redux reducers and realize where this potential bug may surface, because by basing state transitions only on events, these impossible transitions become possible to occur. Sprinkling if-statements all over your reducer might alleviate the symptoms of this:
function userReducer(state, event) {
switch (event.type) {
case 'FETCH':
if (state.status !== 'loading') {
// go to some 'loading' state...
// but ONLY if we're not already loading
}
// ...
}
}
But that only makes your state logic harder to follow because the state transitions are not explicit. Even though it might be a little more verbose, it's better to determine the next state based on both the current finite state and the event, rather than just on the event:
function userReducer(state, event) {
switch (state.status) {
case 'idle':
switch (event.type) {
case 'FETCH':
// go to some 'loading' state
// ...
}
// ...
}
}
You can even split this up into individual "finite state" reducers, to make things cleaner:
function idleUserReducer(state, event) {
switch (event.type) {
case 'FETCH':
// go to some 'loading' state
// ...
}
default:
return state;
}
}
function userReducer(state, event) {
switch (state.status) {
case 'idle':
return idleUserReducer(state, event);
// ...
}
}
But don't just take my word for it. The Redux style guide also strongly recommends treating your reducers as state machines:
[...] treat reducers as "state machines", where the combination of both the current state and the dispatched action determines whether a new state value is actually calculated, not just the action itself unconditionally.
I also talk about this idea in length in my post: No, disabling a button is not app logic.
Difference: declarative effects
If you look at Redux in isolation, its strategy for managing and executing side-effects is this:
¯\_(ツ)_/¯
That's right; Redux has no built-in way of handling side-effects. In any non-trivial application, you will have side-effects if you want to do anything useful, such as make a network request or kick off some sort of async process. Importantly enough, side-effects should not be considered an afterthought; they should be treated as a first-class citizen and uncompromisingly represented in your application logic.
Unfortunately, with Redux, they are, and the only solution is to use middleware, which is inexplicably an advanced topic, despite being required for any non-trivial app logic:
Without middleware, Redux store only supports synchronous data flow.
Source: Redux docs: Async Flow
With extended/UML state machines (also known as statecharts), these side-effects are known as actions (and will be referred to as actions for the rest of this post) and are declaratively modeled. Actions are the direct result of a transition:
When an event instance is dispatched, the state machine responds by performing actions, such as changing a variable, performing I/O, invoking a function, generating another event instance, or changing to another state.
_Source: (Wikipedia) UML State Machine: Actions and Transitions
This means that when an event changes state, actions (effects) may be executed as a result, even if the state stays the same (known as a "self-transition"). Just like Newton said:
For every action, there is an equal and opposite reaction.
Source: Newton's Third Law of Motion
Actions never occur spontaneously, without cause; not in software, not in hardware, not in real life, never. There is always a cause for an action to occur, and with state machines, that cause is a state transition, due to a received event.
Statecharts distinguish how actions are determined in three possible ways:
- Entry actions are effects that are executed whenever a specific finite state is entered
- Exit actions are effects that are executed whenever a specific finite state is exited
- Transition actions are effects that are executed whenever a specific transition between two finite states is taken.
Fun fact: this is why statecharts are said to have the characteristic of both Mealy machines and Moore machines:
- With Mealy machines, "output" (actions) depends on the state and the event (transition actions)
- With Moore machines, "output" (actions) depends on just the state (entry & exit actions)
The original philosophy of Redux is that it did not want to be opinionated on how these side-effects are executed, which is why middleware such as redux-thunk and redux-promise exist. These libraries work around the fact that Redux is side-effect-agnostic by having third-party, use-case specific "solutions" for handling different types of effects.
So how can this be solved? It may seem weird, but just like you can use a property to specify finite state, you can also use a property to specify actions that should be executed in a declarative way:
// ...
case 'FETCH':
return {
...state,
// finite state
status: 'loading',
// actions (effects) to execute
actions: [
{ type: 'fetchUser', id: 42 }
]
}
// ...
Now, your reducer will return useful information that answers the question, "what side-effects (actions) should be executed as a result of this state transition?" The answer is clear and colocated right in your app state: read the actions
property for a declarative description of the actions to be executed, and execute them:
// pretend the state came from a Redux React hook
const { actions } = state;
useEffect(() => {
actions.forEach(action => {
if (action.type === 'fetchUser') {
fetch(`/api/user/${action.id}`)
.then(res => res.json())
.then(data => {
dispatch({ type: 'RESOLVE', user: data });
})
}
// ... etc. for other action implementations
});
}, [actions]);
Having side-effects modeled declaratively in some state.actions
property (or similar) has some great benefits, such as in predicting/testing or being able to trace when actions will or have been executed, as well as being able to customize the implementation details of executing those actions. For instance, the fetchUser
action can be changed to read from a cache instead, all without changing any of the logic in the reducer.
Difference: sync vs. async data flow
The fact is that middleware is indirection. It fragments your application logic by having it present in multiple places (the reducers and the middleware) without a clear, cohesive understanding of how they work together. Furthermore, it makes some use-cases easier but others much more difficult. For example: take this example from the Redux advanced tutorial, which uses redux-thunk
to allow dispatching a "thunk" for making an async request:
function fetchPosts(subreddit) {
return dispatch => {
dispatch(requestPosts(subreddit))
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(subreddit, json)))
}
}
Now ask yourself: how can I cancel this request? With redux-thunk
, it simply isn't possible. And if your answer is to "choose a different middleware", you just validated the previous point. Modeling logic should not be a question of which middleware you choose, and middleware shouldn't even be part of the state modeling process.
As previously mentioned, the only way to model async data flow with Redux is by using middleware. And with all the possible use-cases, from thunks to Promises to sagas (generators) to epics (observables) and more, the ecosystem has plenty of different solutions for these. But the ideal number of solutions is one: the solution provided by the pattern in use.
Alright, so how do state machines solve the async data flow problem?
They don't.
To clarify, state machines do not distinguish between sync and async data flows, because there is no difference. This is such an important realization to make, because not only does it simplify the idea of data flow, but it also models how things work in real life:
- A state transition (triggered by a received event) always occurs in "zero-time"; that is, states synchronously transition.
- Events can be received at any time.
There is no such thing as an asynchronous transition. For example, modeling data fetching doesn't look like this:
idle . . . . . . . . . . . . success
Instead, it looks like this:
idle --(FETCH)--> loading --(RESOLVE)--> success
Everything is the result of some event triggering a state transition. Middleware obscures this fact. If you're curious how async cancellation can be handled in a synchronous state transition manner, here's a couple of guiding points for a potential implementation:
- A cancellation intent is an event (e.g.,
{ type: 'CANCEL' }
) - Cancelling an in-flight request is an action (i.e., side-effect)
- "Canceled" is a state, whether it's a specific state (e.g.,
canceled
) or a state where a request shouldn't be active (e.g.,idle
)
To be continued
It is possible to model application state in Redux to be more like a finite state machine, and it is good to do so for many reasons. The applications that we write have different modes, or "behaviors", that vary depending on which "state" it's in. Before, this state might have been implicit. But now, with finite states, you can group behavior by these finite states (such as idle
, loading
, success
, etc.), which makes the overall app logic much more clear, and prevents the app from getting stuck in an impossible state.
Finite states also make clear what events can do, depending on which state it's in, as well as what all the possible states are in an application. Additionally, they can map one-to-one to views in user interfaces.
But most importantly, state machines are present in all of the software that you write, and they have been for over half a century. Making finite state machines explicit brings clarity and robustness to complex app logic, and it is possible to implement them in any libraries that you use (or even no library at all).
In the next post, we'll talk about how the Redux atomic global store is also half of a pattern, the challenges it presents, and how it compares to another well-known model of computation (the Actor model).
Cover photo by Joseph Greve on Unsplash
Saying redux is like event sourcing, is like saying redux is like a state machine. It's definitely possible to model redux like a state machine, but there's no inherent constraint in redux that promotes this.
The only constraint redux has is that reducers are pure...there's no constraint on actions. Actions can be modeled as commands or events.
Then again, if they're events, where are the commands? And if they're commands, where are the events?
Also where is CQRS? Aggregates?
"Event-driven" does not mean "event sourcing".
After studying more about virtual finite state machines I would say that these patterns that are "good"(citation needed) for distributed systems are just specific instances of something more generic.
I mean, they do work, but they are not as general as they could be, they are specific cases of a more general pattern.
I don't think the author missed "point about Redux being an event store", that point is actually irrelevant for understanding FSMs as they are the more general case, which was the entire point of the article, and I don't think the poster is wrong.
This is what's causing the clash, you are doing something like bottom-up, when the reasoning was top-down.
Top comments (64)
So just to verify my own understanding, Redux is half a pattern because it doesn't fully match up with Finite State Machines.
I think the comparison between the two is "loose" and not a strict distinction and in that sense yes Redux is half a pattern. Or more correctly it shouldn't be directly compared to FSMs, as it isn't a full representation of FSMs.
I think its possible to get Redux to be a "full pattern" that correctly replicates FSMs, but I don't think its something we would want. There's already a verboseness to Redux without specifying how side effects are handled and or how strict transitions are handled.
Also Redux maybe half a pattern, but this is half a full post 😉 (sorry couldn't help myself haha)
PS. I use NgRx, which provides effect handling and cancellations, but this isn't a Redux/NgRx feature, it an Observable/rxjs feature.
Hey Brad 👋🏻. I think what @davidkpiano is trying to convey on Redux to be a "full pattern" is instead of just thinking about the new state; it should embrace side effects as well because any non-trivial app has to deal with side effects. Right now side-effects are treated as second-class citizens and pushed to middlewares to solve the problem. Having this "disconnect" makes it a half pattern.
The same could be said about how Redux doesn't handle displaying the view, or how it handles taking user actions, which are usually React's job.
Redux is a lib, not a framework in itself, it leaves the side-effects up to other libs. Its obviously a design decision that went well with React at the time. React handle the UI, Redux handles the state, async stuff handled by other libs.
Now that React has "moved on" to being able to manage state by itself via hooks, contexts, etc. Looking back it seemed like Redux wasn't "good enough", which may or may not be true, but hindsight is 2020.
The thing with comparing Redux to any other pattern is there is going to be a disconnect, as its job isn't to be a "full pattern", its job is to manage state, and it handles that well.
But the purpose of an application shouldn't be managing state, that's a collateral.
So the fact that redux is a collateral is the failure of an ecosystem.
After all, it was made just to solve a react problem, and to solve the redux problem, you need middleware.
Perhaps there's a need for a complete solution that deal with all aspects of modelling UI.
You have your head deep down CQRS and are not seeing the forest for the trees anymore.
Your right that managing state is collateral of the pattern React produces, where React handles the UI and now something else must handle the state.
Idk if I'd go as far to say "this is a failure of the ecosystem", when such a system is so successful at what it does. You don't need to be the perfect solution, just good enough.
Its easy to point at flaws, point at solutions to those specific flaws, and then state the original system is a failure. As no system is perfect, but the real question is what kind of benefit do you get out of the solution, and is it worth it to move? Are the "failures" that bad?
I'm not sure what a "complete solution" looks likes, idk if anyone does. I'd even say such might just be the "perfections fallacy framework".
"when such a system is so successful at what it does"
I wouldn't call such a system so successful, it does solve a specific problem, albeit also creating more problems. But that could be said about anything.
That's why its a failure of the ecosystem, each system in itself is good and fine, but the sum of all them is less than great.
" Its easy to point at flaws, point at solutions to those specific flaws, and then state the original system is a failure. "
Pointing failures is a bold action when everyone is blinded by hype.
"As no system is perfect, but the real question is what kind of benefit do you get out of the solution, and is it worth it to move? Are the "failures" that bad? "
That's the exact question I started asking, is it worth using React, yes, the failures are bad, in as much as I'm seriously considering parting ways with using react to build user interfaces.
Indeed, I'm already did that, I only use Inferno now, because I migrated to another entire ecosystem, I'm either using ReasonML or Fable with Elm approach for constructing user interfaces and managing messaging and state.
React was only doing the rendering, not even components and JSX I was using, only functions and render.
"perfections fallacy framework"
The entire idea of HTML+JS+CSS is a perfection framework, perhaps I just wanted a canvas and webassembly to make applications, because HTML was made for documents.
Ecosystem failure, yes it is.
Pointing errors is easy, pointing the solution is actual hard.
This industry is built on this idea of disrupting but it won't release the bone, there's no meat anymore in HTML...
(time to bring back VRML, just kidding, but seriously considering)
"Pointing failures is a bold action when everyone is blinded by hype."
I'm not sure what being "bold" with claims has to do with anything beyond ego. Nor am I sure if its reasonable to say React is "blinding by hype" at this point. At a bare minimum, hype pays the bills since React is still popular and being asked for in the job market. At a worse case its a straw man argument ("I'm right because everyone else is blind!")
"The entire idea of HTML+JS+CSS is a perfection framework, perhaps I just wanted a canvas and webassembly to make applications, because HTML was made for documents."
I don't think you understood what I meant by "perfectionist fallacy framework". The perfectionist fallacy refers to the idea "something perfect exists". I never claimed HTML+JS+CSS is a "perfect framework", nor did I claim it itself is a framework.
Furthermore, you claim "the failures of React are bad", then provide 1 example, where "React was only doing the rendering...". Which by itself sounds exactly like what React should be doing. I'm not sure if that is suppose to provide support to the claim "React is a failure", or just provide support for the alternatives you directly mention your moving toward. Unless of course your trying to compare apples to oranges, where you compare React by itself to a more "full" solution.
"This industry is built on this idea of disrupting but it won't release the bone, there's no meat anymore in HTML..."
I don't see how HTML has anything to do with disruptive tech. Or more relevantly, what frameworks do to disrupt really anything other developer viewpoints and egos.
"That's why its a failure of the ecosystem, each system in itself is good and fine, but the sum of all them is less than great."
I can see the ecosystem being multiple parts being itself an issue, but I can also see it as an advantage for those with the manpower and capabilities to leverage each part correctly together, and interchange each part. To label the entire ecosystem a failure, and together "less than great" only works if you give solid examples for all cases. At a bare minimum, it should be agreeable the React ecosystem is successful when used correctly, and its possible you can screw it up with the freedom it gives.
"I'm not sure what being "bold" with claims has to do with anything beyond ego."
It has to do with going against the crowd, that has nothing to do with being motivated by ego.
Its just a strategy, following the safe path versus going slicing the vegetation into the unknown, which has risks, but also benefits.
"Nor am I sure if its reasonable to say React is "blinding by hype" at this point"
Are we past this idea of using React everywhere, so we can start considering where its applicable ? I think not yet.
See all this discussion I caused only because I considered possibly it is not a good answer, and even considered the entire web as not the answer for the problem.
"hype pays the bills"
great argument, paying the bills, couldn't care less about anything technical do we ? better stick with ego motivation, at least it makes sense, anything could pay the bills, even flipping burgers, are we discussing merits of technology choice or what?
"React is still popular and being asked for in the job market"
Oh, the appeal for popularity, that's also a fallacy, its not an argument.
"I'm right because everyone else is blind"
Its not about being right, the moment I start choosing the technology I use based on the problem I need to solve, when others choose based on hype/popularity/job_market, I'm a bit more right, at least in my specific context/use.
I could be wrong or not, but its a bigger mistake to delegate important choices to the crowd.
Its the same argument as the 'ol you can't go wrong by choosing IBM/Microsoft/Oracle or whatever, yeah, technically no, but realistically you are also not right in your choice, popularity is the worst argument for choosing anything.
"I don't think you understood what I meant by "perfectionist fallacy framework". The perfectionist fallacy refers to the idea "something perfect exists". I never claimed HTML+JS+CSS is a "perfect framework", nor did I claim it itself is a framework. "
No, I understood it, I am seriously claiming that HTML/JS/CSS is not perfect, which should've been obvious, people just assume that without even realized, good think you didn't, because that's not what people usually do, it was just a counter-point, perhaps badly executed.
And HTML/JS/CSS are a framework, by definition, they are outside your code, and which totally dictate how your code is organized, they are not libraries, the "web" is a framework, a very bad one for making applications, but a good one for documents, which it was intended to.
And its not about being perfect, the problem with the web tech is that it is the lowest common demonominator, we don't want perfection, that doesn't mean we should accept the lowest possible solution either.
Something better must be possible, I'm betting my business on it.
Some people seem to be too invested to even consider the possibility of something better than the web...
That's their problem, not mine.
" "React was only doing the rendering...". Which by itself sounds exactly like what React should be doing. "
If React only did that... It also has hooks and try to manage state, one of the reasons I ditched it, I only need rendering, not even VDom, what if this idea of having the VDom is a wrong one?
Then that could be considered as a failure, my failure in using React to being with. To be fair, that's not React's failure, but kind of is the ecosystem failure if you go with the crowds (and they are part of an ecosystem) and just use what others are using because of popularity, that was my mistake, it won't happen again.
That wasn't supposed to be an example of were React fails. that's what I mean by ecosystem. React on its own is not bad, but it does create more problems to be solved, that's accidental complexity.
The web is rotten with accidental complexity.
My lens of failure is very simple, is a tool fit for that purpose, React is fit for its purpose, although with too much overhead.
What I claim failure is the composition of the entire ecosystem of modern web-applications, because its clearly not made for that purpose.
Of course React doesn't have anything to do with that, its just that I even consider if the "purpose" of what React achieves was even necessary. That's the question, and there's where's the biggest failure, we are using one thing made for one purpose to another purpose and claiming it is good, when there are clearly examples (like the post) which shows the problems.
When you decision, which should've been no brainer get this reaction...
I've just decided to go against the idea of needing React, not because it is a failure, but because this entire approach for solving this problem is a failure.
"I can see the ecosystem being multiple parts being itself an issue, but I can also see it as an advantage for those with the manpower and capabilities to leverage each part correctly together, "
You have to think hollistically for that, the problem is not the system being multiple parts, I can't context the idea of specialization.
The problem is that the system was made for other purpose and we got stuck in a local maxima.
That's why I said it is not great.
"To label the entire ecosystem a failure"
But it is a failure of being fit for purpose, because we didn't knew the purpose in 2004 when this insanity of using the Web document model to create applications, it all started with AJAX, and even earlier, with DHTML.
No, I don't need to give solid examples for all cases, I could do it, but then this will turn into a master's dissertation, and I have better things to do.
"At a bare minimum, it should be agreeable the React ecosystem is successful when used correctly"
I will agree with that, the system can be used succesful, albeit you have to either ignore the problems it creates, or live with them. I decided I had enough.
"freedom it gives"
There's more freedom in not being restricted by it actually, that's why I went back to "VannilaJS".
For such a long analysis, the article completely misses the point about Redux being an event store that implicitly leverages several good patterns that are seen in highly-available distributed systems.
it forces you to separate your write model (dispatch) from your read model (props). this is known as CQRS.
the reducers are actually referred to as projections in event sourcing. reducers/projections are just aggregate data accumulated from events and are considered part of the read model. the strategy of doing the reduction on each event allows you to present aggregated data in "realtime". the alternative would be to iterate through large sets per read or write. it's a per-event, "realtime" .reduce()
Redux + Thunks was terrible. But Redux + Sagas more closely represent event sourcing as you see it in distributed systems. with events thought of as streams (or aggregates in the Domain-driven Design world). Sagas are actually referred to in the event sourcing world as either process handlers or sagas and are responsible for complex stream handling.
event stores and blockchains use roughly the same structure conceptually: Append only File
except with blockchains, each new insertion is hashed with the previous hash in order to keep the log cryptographically verifiable and immutable-- and is needed for Byzantine Fault Tolerance (so each node can be verified very quickly as a good actor or not).
smart contracts -- the reader contracts have similar logic to reducers. write contracts all look like event dispatch.
consensus algorithms do exist outside of blockchain tech. and with data structures that are considered high availability (they cluster and replicate), you will see that they are all AoF or KV stores or a combination of both.
most databases under the hood actually have a very similar event sourcing structure referred to as WAL -- write-ahead logging.
the typical event payload is the same in all these systems {eventType, metadata}
Just take a look at Redux w/ Sagas, Greg Young's Event Store, any blockchain, and even Kafka. They are all fundamentally similar. And since they all source from a log file, they can all replay state.
Also, Redux is an opt-in and can be as specific as you want based on how you tag your events. Your entire app doesn't need to respond if you slice your state right. And the structure itself has proven to be efficient. Event sourcing systems can handle millions of concurrent streams. Sure, people might argue there are optimizations in infrastructure that are NOT in Redux, but the structure itself is pretty ideal for streams if you write good handlers and partition your aggregates in a sensible way. The people who wrote Redux Saga totally see it.
I'm actually surprised that Abramov doesn't know the source inspiration for the thing that made him famous in the community.
There's two parts to this article, Rodolfo. Be patient.
Excuse me? I'm sure you think I'm rude, but this article is unnecessarily long and didn't touch on much other than how confused people are (which surprisingly include people on the React team). My 1 comment has far more insight into the implementation than your entire article. Why don't you just take the criticism? It's obvious you're not crystal clear on this either and you went down a rabbit hole and will probably end up using a lot of the insight I just gave you (and I'm sure what other commenters have given you).
In fact the only reason I signed up was to comment at how bad the article is because I felt that it was prolific enough that it does people who want insight a disservice. Will you really make an awesome part 2 that isn't an inane, self-serving, ambiguous CS analysis of a practical pattern that will help clarify it for people and give them insight on when and how to use it? I really hope so because you have a great opportunity here to teach.
Hint: Immutability and finite state machines will probably confuse people more. Save that for part 3 or a deep dive on structures. Event sourcing is a pretty misunderstood pattern and sub patterns have emerged.
Redux is definitely not used for event sourcing, nor is event sourcing applicable for most of the applications Redux is used for.
Thanks for your comments, though!
You don't understand the pattern. Don't mislead an entire group of people.
You are storing events. There is a literal "store" prop passed into Provider. Therefore it is a "source" of events and not just ephemeral event-driven programming like you see in video games.
It is event sourcing.
You just proved to me that you don't understand event sourcing.
You don't store state with ES (except for snapshots etc). You store events. That is impractical for most apps.
I think you're just Googling event sourcing and trying to disprove me. Because you can most definitely store "state" with the pattern and it is done quite often. They're called projections. State storage is your read model, actually. For people who just need to read from streams, they don't need projections in-line with the event store. They do aggregations in another service. We just see the entire flow in Redux.
I mean you're kind of proving to me you haven't thought of this pattern much and you're about to try and teach people.
Go write a post then.
I am working on it.
But I'm not prolific and it is something I know that gets even the event sourcing community heated. Only reason why I chimed in is because you are prolific and I think you have a chance at giving a ton of people an aha moment that isn't in the wrong direction.
If you want to research for your follow up blog post, check out how similar these are to Redux and Redux sagas:
In particular, the event handlers:
github.com/commanded/commanded
Projections:
eventstore.org/docs/projections/in...
Discussion on sagas and handlers:
stackoverflow.com/questions/342846...
Can't deny how close the patterns match up. I mean the EventStore Projection API is basically what we know as a reducer. Event Store was made back in 2012.
But I will say that in infrastructure, people misunderstand and do all sorts of weird event sourcing patterns. If you had a good evented model in your infrastructure that represented state pretty well, complex stream transforms in Redux (and Redux itself) starts to disappear because the work has been done for the UI.
The only reason why we needed Redux in the first place was because we needed to model some type of complex state representation that the backend didn't give us. If the state is computed in the infrastructure and the UI as a service just receives it, where is the role in Redux other than acting as a global store or a pubsub? And even then, with GraphQL and Apollo + cache, Redux's complexity and importance starts to disappear.
Time to eat some much needed humble pie Rodolfo; you want to push your point of view so hard...
Redux is absolutely not event sourcing (I used both extensively)
The only thing in common is that there are "events", something extremely common and found in pretty much all codebases (and the dreaded DOM)
No frontend keep all the events from last week around for the logged user, it's slow enough as it is. If you don't keep all the events, then it's not sourcing, just event driven (like almost all UI paradigms from the last decades)
Trying to find similarities between very different architectures might help you learn new things, but there's not point pushing this tool down people's throat.
You wanna mansplain me more? Maybe you can explain how React works, too. Insult me because you think I'm using analogies as learning tools (although that is absolutely completely valid)?
Actually, I think you should eat some humble pie. I am bringing up a legitimate comparison and I have used both extensively as well.
Your argument doesn't make sense and is just a nitpick. You in fact DO keep all the events in context of the lifetime of your frontend. If you do not choose to persist them after the browser closes or refreshes, that's your choice. But the implementation itself matches event sourcing. You are using an event store, running projections off of them, and using those projections in your read model. You can even replay your events to regenerate your state. If it looks like a duck, talks like a duck?
Also, there are event streams that terminate in event sourcing, you know. You could think of a browser session as a unique event stream with a prefix "ui-1", "ui-2", etc.
But again, the main point is: in context of the UI's session lifetime, the events are not ephemeral and you are indeed keeping all the events.
As for speed, the implementation of Redux might be slow, but updating state per event is one of the reasons why people use event sourcing in infrastructure-- to do relatively complex and flexible compute "realtime" without having to iterate through large sets.
The "slowness" is due to the growing event store which is a problem in ALL event sourcing implementations. In infrastructure, you typically cold storage "really old events".
If you don't have the flexibility to see the valid comparison, I doubt you've used both as much as you say or you haven't really thought about it. The similarities go FAR beyond analogies and learning helpers.
In fact, if you took the event storage itself and persisted it, you now have a way to recover complete session state through replay. Though UI state isn't so complex or critical that you can't just persist the state itself versus doing a replay from the events. It would be useful if you wanted to track clickstream and state transitions beyond 1 session. Which some people might want to do.
As I said in a previous post, other than purely UI state, we needed Redux to model complex domain state that the infrastructure should have already given us. If the we already had that state, we wouldn't need to mirror what many scaled infrastructures use for our frontend. The reason you remove Redux is you already HAVE the state you need. But I think the pattern itself is a pretty important entry drug (although contentious because people do it in so many different ways) concept that could help launch people who know React and have only worked in React to understand how distributed systems manage state and data.
React is already very good training in SOLID. And as a result, a lot of the component patterns you see fundamentally match up closely with supervision tree models in distributed systems. Now THIS next statement is me using an analogy, but if you diagram a supervision tree and don't label it and just describe it in terms of supervision tree definitions, then tell people to label it in terms of React Components, they will surprisingly know how to.
@Rodolfo you’re my hero 🤙🏼
" If the state is computed in the infrastructure and the UI as a service just receives it, where is the role in Redux other than acting as a global store or a pubsub? And even then, with GraphQL and Apollo + cache, Redux's complexity and importance starts to disappear. "
Well, you are not wrong about this.
That has nothing to do with the fact of things being event store or not, its just that you don't need to store collateral state if you model the data for the consumer perspective, instead of the producer, this should be obvious.
When all you have is an "EventStore", everything looks like events.
The only problem with Eventstore and CQRS is that FSM are actually the generalization, not the other way around.
With the David actually got right, and you didn't, Rodolfo.