loading...
Cover image for Redux is half of a pattern (1/2)

Redux is half of a pattern (1/2)

davidkpiano profile image David K. 🎹 ・15 min read

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 and error 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 the LOAD event occurs, you immediately transition to the loading 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

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 where temperature === 90 (celsius)
  • solid where temperature === -5
  • gas where temperature === 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:

State machine describing login flow

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:

  1. Search for anything.
  2. Click on the "Posts" tab while the search is happening.
  3. 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):

Reddit bug showing no search results

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.

Source: Redux style guide: treat reducers as state machines

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

Discussion

pic
Editor guide
Collapse
bradtaniguchi profile image
Brad

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.

Collapse
icanrelate profile image
chriz

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.

Collapse
bradtaniguchi profile image
Brad

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.

Collapse
salvoravida profile image
Salvatore Ravidà

The power of Redux is its low level api, and middleware support.

We have to assist every days to tons of common places about Redux.

Thread Thread
davidkpiano profile image
David K. 🎹 Author

The low-level API is fine... that it enforces a single atomic global store is where unnecessary complexity grows.

More on that in part 2!

Thread Thread
salvoravida profile image
Salvatore Ravidà

You are missing the point, that "reducers" and "combineReducers" are de-facto the way of split a "global Store" into many "subStores" but with theese advg:

1) every "subStores" can listen every action/event and so react to that if needed
2) you have a centralized way to debug/control your app via DevTools.
3) you can access/interact with state outside of rendering lib-> think to channels, inter-framework communication - etc...

You can understand/debug all-business-logic of an "well-projected-redux-state" app, in few minutes, just opening Redux DevTools.

Moreover you can use As many Stores as you want, just as people use as many "React.Context" by using many <Provider !

The main advantage is to have 1 SINGLE "ROOT" of THRUTH!

Note "Root" not single Store.

That Said,
Decoupling business logic from render is something that every software architect should think about. Today react is the best rendering lib, but nobody knows tomorrow.

Redux and middlewares is perfect to Decoupling business logic from rendering.

WhatYouSee=Function<React>(state)

Just as Generics, tomorrow just change "React".

Thread Thread
davidkpiano profile image
David K. 🎹 Author

Okay, please keep using Redux however you'd like, thank you for your comments

Thread Thread
bradtaniguchi profile image
Brad

I do find it fascinating how there are very drastic responses to this post so far I must say. Some people agree that Redux is inherently "wrong", and other argue its inherently "right".

I'm like many, waiting for part two :D

Thread Thread
blocka profile image
Avi Block

The main advantage is to have 1 SINGLE "ROOT" of THRUTH!

Not sure about you, but in the types of apps that I write, I can not see any advantages of having a "single source of truth". In general, the complexity is always either at the page level, not the app level (the app is essentially just a router with some small global data, and an entity cache), or within some sub components on each page.

So I don't need a "single state atom" for anything. What I do need is to manage the state transitions of those complex components. Redux does nothing for me here. XState does tons.

I suspect most people are using redux as a global entity cache, or worse...what you do to store "global" data.

Collapse
rodocite profile image
Rodolfo Yabut

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.

Collapse
davidkpiano profile image
David K. 🎹 Author

There's two parts to this article, Rodolfo. Be patient.

Collapse
rodocite profile image
Info Comment marked as low quality/non-constructive by the community. View code of conduct
Rodolfo Yabut

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.

Thread Thread
davidkpiano profile image
David K. 🎹 Author

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!

Thread Thread
rodocite profile image
Rodolfo Yabut

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.

Thread Thread
davidkpiano profile image
David K. 🎹 Author

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.

Thread Thread
rodocite profile image
Rodolfo Yabut

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.

Thread Thread
davidkpiano profile image
David K. 🎹 Author

Go write a post then.

Thread Thread
rodocite profile image
Rodolfo Yabut

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.

Thread Thread
boubiyeah profile image
Alex Galays

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.

Thread Thread
rodocite profile image
Rodolfo Yabut

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.

Thread Thread
joeyorlando profile image
Joey Orlando

@Rodolfo you’re my hero 🤙🏼

Collapse
blocka profile image
Avi Block

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".

Collapse
oliverradini profile image
OliverRadini

Clearly a very well thought out post, with some interesting points.

I'm struggling to agree with the main theme here, though; I can't understand why the fact that Redux doesn't explicitly follow the state machine pattern is a problem. Redux is a library for state management, right? Some types of state might be best represented as state machines, but is this the only way to represent state? Probably not. And I'm guessing that state machines aren't always the best way to manage state, either.

State machines are one type of abstraction which one might apply over state, and whatever-pattern-redux-should-be-labelled-with is another. Pick your poison. Yes, you can introduce bugs into an application with Redux, some of which might be prevented with a state machine pattern, but I imagine the reverse is true, also.

What I personally like most about Redux is it forces state into a subset of the language which is easy to manage. Pure functions and values are simple, and easy to reason about.

The main problems I see with Redux are:

  1. Trying to do too much with middleware
  2. Only using Redux because you wanted to map props in deeply nested components

Regarding 1., I don't see why Redux should need to contain any async logic at all. Put it elsewhere. Regarding 2., there are better ways to do this and it's not really the point of using Redux at all.

Collapse
davidkpiano profile image
David K. 🎹 Author

I don't see why Redux should need to contain any async logic at all. Put it elsewhere.

This is the main problem I'm trying to highlight. Fragmented logic because of an incomplete pattern.

Collapse
oliverradini profile image
OliverRadini

Sure - but if Redux took a more holistic approach, and went so far as to implement state machines, then it'd be a different library. There are options out there which do give you state machines in javascript, for instance XState - which you develop (as an aside, I think it'd be better if you state quite explicitly that you're the developer of that library, for the sake of full disclosure).

In the example you give of Reddit's loading state, we might equally view whether or not there were no results as an item of derived state. It can be derived from two items of required state; the set of results, and the loading state of the application. Something like:

const hasNoResults = ({ results, loading }) => !loading
  && results.length === 0;

Not saying this is a better approach, just a different one, and a different solution to the same problem. Different perspectives will yield different results and it's hard to give very clear guidance as to which is better in all situations, and I think that calling Redux half a pattern is a little misleading. It's half of the pattern you decided to compare it against, or, it isn't a pattern which could be called state machines.

Thread Thread
davidkpiano profile image
David K. 🎹 Author

as an aside, I think it'd be better if you state quite explicitly that you're the developer of that library, for the sake of full disclosure

I didn't want to do this because I just wanted to talk about Redux and state machines, instead of comparing/promoting my library. I didn't think that would be fair.

Not saying this is a better approach, just a different one, and a different solution to the same problem.

Of course! You can definitely write your logic like this, without explicit state machines, and cover all the edge cases, and have it work just as well as an explicit state machine approach. It just becomes much harder to "reason about" and understand how the app flows from a higher level. But if you don't care about that (and you just want to ship stuff the way you know best), then that's fine.

It's half of the pattern you decided to compare it against, or, it isn't a pattern which could be called state machines.

No, it's half of a pattern because it provides lightly opinionated "scaffolding" (atomic global state, reducers are pure functions with no constraints except for purity, side-effects via middleware somehow) but the other half of the pattern is what the user needs to implement themselves. They need to "fill in the blanks" and that leads to them constantly half-inventing state machines, whether they know it or not.

Like I said, the comparison against state machines isn't an arbitrary choice. Our UIs are state machines.

Thread Thread
oliverradini profile image
OliverRadini

I do still think it's relevant to mention that you've developed that library. Not to say that it makes you biased; it makes you informed on state machines and the strengths/weaknesses of that pattern.

Anything that's half a pattern could be considered a whole of some other pattern. The boundaries between patterns are completely arbitrary and we can draw the lines where we like.

I wholly disagree that using derived state is less reasonable than state machines. The two aren't mutually exclusive in any case, but both patterns have their supporters and theory, both patterns are likely equally able to be reasoned about. Indeed both are likely to be little more than constraints which can be applied to state management, and it's quite possible that both make an application's state a more reasonable proposition.

Thread Thread
davidkpiano profile image
David K. 🎹 Author

I wholly disagree that using derived state is less reasonable than state machines.

But it is! The proof is that derived state can determine "what state are we in", and finite state machines can also determine "now that we're in this state, what can happen next?" That part is valuable information, and very hard to determine without some sort of abstract model.

Thread Thread
oliverradini profile image
OliverRadini

Indeed you're correct; though to reiterate what I said in the last reply, both are restrictions we can apply onto state which are likely to make it more reasonable. I don't think they're anything like mutually exclusive.

I had meant for derived state to be little more of an example of a different pattern which you could measure Redux and/or state machines against. My point was intended to be that these types of comparisons depend on the reference points of those comparisons; Redux is 100% of one pattern and 50% of another, and 0-99% of yet more patterns.

Collapse
jontymc profile image
Jonathan Curtis

Interesting post and made me clarify my thoughts about Redux. I would argue that actually Redux is trying to be more than it should and this is a big source of confusion to people trying to learn it.

For example, whether you use a state machine or not should have nothing to do with the state itself. Why do you need a reducer? Just have your own state machine logic and update the state using events from the state machine.

Redux should just really be about updating state with events, reducers muddy the waters. I think this muddiness comes from its origins of use with react and specifically the idea of uni-directional data flow. Practically, state management should be agnostic of uni-directional data flow, but this is entrenched in Redux design.

Middleware and async actions also lead people down the wrong path. Mostly this is just code you should have in plain code you write. Eg, loading data should be outside of state management. Load data, update state. Don't complicated with middleware.

I prefer libraries that do one thing well. A state management system should just be about events, state and subscribing to changes. Redux has confusing concepts like reducers, async actions and middleware, not to mention actions are events. It would be much better split into two parts - state management and state machine, although arguable whether you need a library for the second part.

Collapse
jontymc profile image
Jonathan Curtis

I would also argue that the loading state of an api request does not belong in the global state. This is local to a single component, putting local un-shared state in the global state makes things more complicated.

Collapse
davidkpiano profile image
David K. 🎹 Author

Completely agree. I'll cover this more in the second article. Nothing belongs to global state because nothing truly is global state.

Thread Thread
boubiyeah profile image
Alex Galays

That's my main gripe with redux.

I think no more than 20-30% of state should be global: logged user and its organization, resource cache and that's pretty much it.
It's insane to have to go through all the selectors for some fairly local piece of state changed.

Collapse
helloitsjoe profile image
Joe Boyle

Great post! I went from "why is he calling actions events" to "why weren't actions called events in the first place??" I love the idea of using a status enum instead of a combination of loading, error, data, etc.

One thing... Unless I misunderstand what you're saying about canceling an async thunk request, you can just ignore the success action if the fetch has been canceled:

  switch (action.type) {
    case "FETCHING":
      return { ...state, status: "fetching" };
    case "FETCH_SUCCESS": {
      if (state.status !== "fetching") {
        return state;
      }
      return { ...state, status: "resolved", users: action.payload };
    }
    case "CANCEL":
      return { status: "idle", users: [] };
    default:
      return state;
  }

Here's a working codesandbox: codesandbox.io/s/redux-thunk-cance...

Looking forward to part 2!

Collapse
letanthanhtai profile image
Le Tan Thanh Tai

Very well written. I’ve just come up the very same idea that redux middleware as state transition functions and shared it to my colleagues then I came to this articles.

Keep up your good works.

P/S: On the other hand, I also think that a middleware is really a controller which is missed in MVC model. A very limited controller which is responsible for UI triggered changes and API triggered changes

Collapse
jacobweber profile image
JW

This is great....very well-written and clear. I gave up on Redux early because of my confusion about async actions....having to install some kind of third-party middleware just didn't feel right. Looking forward to the second part.

Collapse
cdoublev profile image
Guillaume

Hello David,

I have been reading FSM related posts (including yours) with great interest since a few years now, but I've never taken the step to include this pattern in my projects. I feel like there's more boilerplate and that it's a bit overthinking for simple UI component.

Anyway: "[Transitions] can be represented using boxes and arrows" makes me wonder if you've ever thought about implementing FSM using category theory and algebraic data structures?

Collapse
xstos profile image
Chris C

I inherited a react/redux app here at work, and I found the organizational structure really hard to follow due to how everything was coupled. It had code smells all over it. The ideals were good, but the implementation left a lot to be desired.

Code is for people to read, so if patterns are confusing I disregard them and ignore "the experts". Amusingly the best and most understandable software pattern i've happened on in my career has been a mix of all these patterns.

C# has part of the answer with extension methods. You define data inside classes, and then create transformers (extension methods) between types (basically your entire codebase is a giant LINQ library). If the data being operated on is immutable, and you undo all the mistakes the OO architects created by getting rid of nulls, using discriminating unions, not using exceptions (or wrapping them at the lowest possible levels) and much more (fsharpforfunandprofit.com/fppatterns/) you end up with really modular code.

For the UI you use a hydrid model, where you make your datastructures implicitly serializable (as lispy as possible) with react-ish UI generator functions to render atoms of UI (again you end up with a LINQ library of functions to build UI very similar to react but without needing the jsx syntax). If you memoize the important bits, then your UI doesnt get recreated over and over. I then use a global event pipe for all the events. It all works great.

The only problem is, I've met precisely zero people that program this way, by stealing a bunch of patterns and mashing them together. FP abstractions sometimes get so into the weeds, that it's hard to read, so you have to strike a balance between all these aspects, by continuous refactoring and no upfront design. This is obviously really hard with more than one developer as the bad habits everybody has breaks all this stuff in a myriad of ways.

Rich Hickey has a talk where he states "simple is hard". That's what I do. I make everything so stupidly simple, it's modular as hell. Then I go to work, and get inundated with OO spaghetti code, and it saps the fun right outta life...

Collapse
oliverradini profile image
OliverRadini

Do you think that Redux falls into the bracket of complex and easy, rather than difficult and simple? (to use the categories defined in Rich Hickey's talk)

For me, it falls very much within the bucket 'simple and difficult'. It has a single representation of state, which is a value. State can only be updated by reducers, which are pure functions. Reducers can only be triggered by actions, which are just data.

I'm struggling to see what part of that is complex, and I've spent a lot of time trying to think about whether or not Redux is simple or complex. How it gets integrated with React components might be a little more complicated on the surface, but again this is basically always done using higher order functions, which are an abstraction of sorts, but relatively straight forward.

Please do correct me where I'm wrong because I'm not trying to just pick holes in what you say here, I agree with the rest of your comment entirely, I'm just trying to objectively question where Redux falls along the simple-complex spectrum.

I found that Redux became a lot more appealing to me personally once I sat down and properly read the paper 'Out of the Tar Pit'. I like the idea of silo-ing state and restricting its manipulation to a subset of a language.

Collapse
xstos profile image
Chris C

Hmm good question. I think the conceptual model is definitely simple, but as you say, since it's only a piece of the pattern, many devs opt for obtuse boilerplate (ignoring DRY), rather than buckling down to build higher level abstractions to achieve KISS and make code easy to reason about and navigate/digest. They are afraid to write utility code, or code generators (because it's perceived as hard) to essentially be as lazy as possible and write the least code. So they are lazy and wasteful instead and accrue tech debt and increase illegibility. Most devs don't write their code for humans to read with the simplest conceptual model. I love plain functions. Simple data, single argument, no side effects, simple data out. Most code should literally look like an excel spreadsheet. Classes are table rows, lists of classes are tables, formulas are static mapping methods to new tables. Unidirectional and simple.

My gut blames encapsulation as well. If one can't easily open an engine, and mod it, then we get these weird unreadable codebases like Dan Abramov was referring to. Everybody is solving a small slice of the problem and a lot of the problems internal to the project are hidden and not exposed to the community. I noticed this when I re-implemented entire utility libraries that I later noticed Microsoft had in their own reference source (often with the same signatures), but had not shared. So we get all these npm snapshots of pieces of the problem and people repeating the same solutions all over the place and they're hard to hook together and model cohesively. A big pet peeve of mine is that all code should be public and modifiable from anywhere. Swim not dive.

I suspect encapsulation doesn't scale well, because when one is 200 levels up from a problem (the npm packages folder depth is insane), perspective is lost. My gut says that if there was an R&D group that built an app from the ground up modeling everything like a physicist would, be it temporal logic (reactive extensions), asynchrony (promises), UI component models like react, the mapping of state to UI with redux, the low level rendering of graphics, etc. All those things would need to be in the same codebase to evolve together (as a modelling exercise). The fact that our dev environments don't come with hygienic macros or simple code generators built in (controversial I know), means we can't create DSLs to solve all our issues with minimal bloat when the libraries don't quite capture our requirements.

Unfortunately the difficult nature of code sharing and collaboration fragments this and separates concerns that perhaps shouldn't be separated. Not sure much of this diatribe helps anything though D:

PS: I am not a DRY extremist, I lazily model problems by how many times I see code repeating. Ad-hoc, on the fly with little forethought. Only when my bad code spidey sense tingles, do i step back and think about things. The repetition hints at missing conceptual models and informs what abstractions I create. This is really hard to do collaboratively however, as it requires training people to use a ruthless refactoring algorithm while they code. I still haven't had any luck teaching people how to do this at scale. I've never really taught people anything en-masse, so i'm sitting here on my high horse when perhaps I should be trying to convert a few people at work and see what comes of it. :)

Collapse
offbeatful profile image
Dennis Miasoutov

Nice article! The moment I saw Redux - I started thinking on building state machines. That resulted in this nice library: github.com/mocoding-software/redux...

It allows build finite state machines using Redux. Please check this out.

Collapse
pettermahlen profile image
Petter Måhlén

Interesting article, looking forward to the next part!

Have you seen gist.github.com/andymatuschak/d5f0...?

In github.com/spotify/mobius, which I interpret as similar to the ideas you're describing, we decided to not mandate modelling 'state' as an FSM, though we think that FSMs are sometimes the best model (cf github.com/spotify/mobius/wiki/Def...).

Collapse
davidkpiano profile image
David K. 🎹 Author

Hi Petter, yes I've read that a while ago, interesting implementation! Spotify's Mobius (thanks for showing it to me!) also looks interesting, I'll take a deeper look at it.

Collapse
beezlebuddy profile image
Ryan

Thanks for this, David. I know these has been some shade cast in some of the comments (yOu ArE mIsSiNg ThE pOiNt), but I love this thinking outside the box and saying "redux can be this", and I love using finite state machines in other code basis, so there's a good chance I am going to march to the beat you are banging out.

I'm very much looking forward to your next post.

One question, though: I've always daydreamed about working in Orlando. Do you work remotely from there or do you "go to work" in an office there? I didn't think there were many dev jobs down there.

Collapse
_ezell_ profile image
Ezell Frazier

In layman's terms, the combination of current state and an event decides whether a state-update occurs within a finite state machine?

So we essentially have a table. The columns are possible states, and rows being what can happen based on an event?

Collapse
jwp profile image
John Peters

This complexity vaporates when we stick to the much simpler event, eventhandler pattern. Events emit data and subscribers handle the notification. Components are small and only do one thing. For example, a Person component works with a Person model and A person object. That's it. This component is responsible to get the person object, subscribe to the result in populate the person model. Later it may contain a FindPerson function which has a well defined input and output interface. The person component grows over time and may provide notifications at any time. Because events are loosely coupled, what happens next is of no concern to this component. Simple concept, used by every browser in the industry why not follow what already works? Event s have been around for 50 years. They are the original observer pattern.

Collapse
morsmodr profile image
morsmodr

Can't wait for part 2. I guess one can either make an argument that redux is incomplete or the fact that in large scale applications, redux forces one to write way too much code and makes it easy to cause issues with respect to transitions. We inherently branch out code over time to handle various transitions and FSM approach makes my mind feel at peace.

Regardless, the root cause is with how side effects are handled. And I have loved whatever I have read in this article. Long, but worth the effort. I'm maintaining a large scale application which uses redux-observable to manage side effects and also have to consider state variations across various markets, resulting in a lot of If checks throughout the application. Been looking to implement state machines on top of redux-observable, and I'm not sure that's the right approach. Maybe scratch redux altogether and go all in with state machines and statecharts is another thought, but given its a large app - the thought is scary! Haha.

Again, looking forward to part 2.

Collapse
longlho profile image
Long Ho

Have you checked out martinfowler.com/eaaDev/EventSourc... specifically the External Updates section?

Collapse
mrpmorris profile image
Peter Morris

I used NgRx for about a year. I found it unpleasant because I had to write so much boilerplate. When Blazor came out I decided to write a Flux library that took advantage of OOP principles (no need for switch statements, for example).

I came up with Fluxor - no-boilerplate, and UI agnostic. It's far less work.

Collapse
iurijacob profile image
Iuri Jacob

Very nice post. Never had thought in redux as a finite state machine, but it totally makes sense. Enlightening... Waiting for the second part.

Collapse
techpeace profile image
Matt Buck

If you're looking for a similar approach for Vue, I'd recommend checking out this post about using Vuex with the xstate library.

Collapse
farhd profile image
Farhad

Thank you for sharing your thoughts 👍
You might want to check these out:
sam.js.org
meiosis.js.org
github.com/jeffbski/redux-logic