DEV Community

Cover image for Redux is half of a pattern (1/2)
David K. 🎹
David K. 🎹

Posted on

Redux is half of a pattern (1/2)

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

Latest comments (64)

Collapse
 
luiz0x29a profile image
Real AI

"Water can't be both liquid and gas simultaneously"
Well, it actually can.
thermal-engineering.org/wp-content...

I wonder why no one mentioned it.

Collapse
 
chadsteele profile image
Chad Steele

I'd love it if you wanted to dissect my Redux one line replacement hook... useSync
dev.to/chadsteele/redux-one-liner-...

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
 
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
 
cdoublev profile image
Guillaume • Edited

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
 
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
 
morsmodr profile image
morsmodr • Edited

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
 
farhd profile image
Farhad • Edited

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

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

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

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

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

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
 
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
 
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 • Edited

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

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.

Thread Thread
 
luiz0x29a profile image
Real AI

Partial update of state in immutable pure functional applications is pretty much open research.
We actually need machinery to deal with it, you can't just update the global state every time.
You just happen to now have the same problem again, instead of React and DOM, you reintroduced it with your entire application global state in place of the DOM, and now need some form of React to deal with it.

Perhaps the entire perspective was wrong.
Virtual Finite State machines is what I'm looking into, they contain the mutable state, the are pure in the way that state+event change state, and they allow for side effects, because without side-effects:

"Haskell is a completely useless language because in the end a program with no effects, there's no point in running it, is a useless program.
And you know, you have this black box, and you press go, and it gets hot, but there's no output.
So why did you run the program" - Simon Peyton Jones

youtube.com/watch?v=iSmkqocn0oQ

Some comments may only be visible to logged-in visitors. Sign in to view all comments.