Logo

dev-resources.site

for different kinds of informations.

Reducer vs. Finite State Machines: Understanding the Paradigm Shift

Published at
3/25/2024
Categories
javascript
frontend
state
statemachine
Author
ibrocodes
Author
9 person written this
ibrocodes
open
Reducer vs. Finite State Machines: Understanding the Paradigm Shift

Alex boarded the bus with two of his kids. The kids were disturbing everyone around, but Alex remained silent. At this point, everyone grew angry with Alex, assuming he wouldn't discipline his kids, thus deeming him unreasonable.

Then, a fellow passenger shouted at Alex about the whole situation. Absent-minded, Alex realized what was happening and apologized to everyone. He added that his wife, the mother of the kids, had just passed away, and he was deeply troubled, oblivious to his surroundings as a result.

At this point, everyone around understood the situation and felt remorseful for hastily judging Alex's character without understanding his circumstances. If they had known about Alex's state, they would likely have behaved differently. Instead, they focused on the event—the playing children—and reacted, something most of them later regretted.

This phenomenon, from the field of Social Psychology, is called the Fundamental Attribution Error (FAE). It involves judging a person's behavior solely based on their disposition, such as temperament and personality traits, while completely ignoring their situation:

In social psychology, fundamental attribution error, also known as correspondence bias or attribution effect, is a cognitive attribution bias where observers underemphasize situational and environmental factors for the behavior of an actor while overemphasizing dispositional or personality factors.
Source: Fundamental attribution error (Wikipedia)

In this article, we'll delve into the paradigm shift between the reducer and finite state machines models and explore how the FAE can help us make more sense of these patterns.

The Reducer Paradigm

In our earlier story, the fellow passengers didn't take into account Alex's state before reacting to the children's disturbance. Their response was solely based on the event.

Similarly, the reducer model operates on an event-first approach, focusing solely on the episode and responding accordingly. React's useReducer() hook and state management libraries like Redux rely on this paradigm.

When developing a straightforward application like a counter, the reducer pattern poses no challenges. You can easily employ React's useReducer() hook and design logic for actions such as ADD_COUNT, REDUCE_COUNT, and RESET without needing to consider additional states—life is good.

However, what about more complex applications, such as an API-call logic that can exist in multiple states? The reducer logic for such an app might resemble the following:

const reducerFetcher = (state, action) => {
  switch (action.type) {
    case "LOAD_DATA": {
        return { ...state, status: "loading" }
    }
    case "LOADED": {
        return { ...state, status: "success" }
    }
    case "CANCEL": {
        return { ...state, status: "idle" };
    }
    case "RELOAD": {
        return { ...state, status: "loading" };
    }
    case "ERROR": {
        return {...state, status: "error" }
    }
    default: {
      return state;
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

By simply examining this logic, you might notice some issues. For instance, if the LOAD_DATA event fires while the app is already in a loading state, the application would initiate the loading process anew. This indicates that the reducer is open to processing any event at all times, which is not efficient. We need to take into account not only the event but also the current state to ensure more effective handling.

The Finite State Machine Paradigm

Back to our story. If the passengers had been aware of or inquired about Alex’s mood, they would have known how to react more appropriately. This could have spared them the burden of anger and frustration, as well as the embarrassing moment when they learned about the man’s situation.

The finite state machines pattern circumvents the Fundamental Attribution Error by prioritizing a state-first approach. This model evaluates the current state before determining how to respond to an event.

Every application you've ever built operates within a finite number of states, whether you've consciously acknowledged it or not. Take, for example, an audio player. At any given time, it can only be in one of several states: idle, loading, playing, or paused.

Each state permits a subset of all possible events within the application. Events can also trigger a change in state, known as a transition. In addition to transitions, events can prompt actions such as invoking a function, generating another event, or executing asynchronous operations.

Now, let's reimplement the same API-call logic we previously discussed using the useReducer() hook, but this time adhering to the principles of finite state machines. There are two ways to accomplish this:

The Implicit Way

const reducerFetcher = (state, action) => {
  switch (action.type) {
    case "FETCH": {
      if (state.status === "idle") {
        return { ...state, status: "loading" };
      }
      return state;
    }
    case "FETCHED": {
      if (state.status === "loading") {
        return { ...state, status: "success" };
      }
      return state;
    }
    case "CANCEL": {
      if (state.status === "loading") {
        return { ...state, status: "idle" };
      }
      return state;
    }
    case "RELOAD": {
      if (state.status === "success" || state.status === "error") {
        return { ...state, status: "loading" };
      }
      return state;
    }
    case "ERROR": {
      if(state.status === "loading"){
        return {...state, status: "error" }
      }
    }
    default: {
      return state;
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Indeed, while we continue to evaluate events, we now prioritize checking the state before reacting to them, ensuring that certain events are restricted to specific states, which aligns with our objective.

However, the logic becomes convoluted, making it challenging to discern the current mode of our application and the events associated with individual states. Moreover, as the application scales, the potential for nested if/else statements increases, making it difficult to manage and prone to errors.

The Explicit Way (Better Approach)

const initialState = {
  status: "idle",
  data: undefined,
  error: undefined,
};

const fsmReducer = (state, action) => {
  switch (state.status) {
    case "idle": {
      if (action.type === "FETCH") {
        return { ...state, status: "loading" };
      }
      return state;
    }
    case "loading": {
      if (action.type === "FETCHED") {
        return { ...state, status: "success" };
      }
      if (action.type === "CANCEL") {
        return { ...state, status: "idle" };
      }
      if (action.type === "ERROR") {
        return { ...state, status: "error" };
      }
      return state;
    }
    case "success": {
      if (action.type === "RELOAD") {
        return { ...state, status: "loading" };
      }
      return state;
    }
    case "error": {
      if (action.type === "RELOAD") {
        return { ...state, status: "loading" };
      }
      return state;
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Instead of switching based on the event, we're now switching based on the state. This approach allows us to reason with our logic more easily because the states and their associated allowed events are clearly defined.

However, upon closer inspection, we're still applying the reducer pattern, but this time only locally within individual states.

Embracing the well-established finite state machine paradigm can significantly alleviate the pains of development:

The traditional techniques (such as the event-action paradigm) neglect the context and result in code riddled with a disproportionate number of convoluted conditional logic (in C/C++, coded as deeply nested if-else or switch-case statements). If you could eliminate even a fraction of these conditional branches, the code would be much easier to understand, test, and maintain, and the sheer number of convoluted execution paths through the code would drop radically, perhaps by orders of magnitude.
Source: Who Moved My State?

Conclusion

Finite state machines offer a robust solution for managing complex behaviors in event-driven applications. While the reducer pattern provides a solid foundation, FSMs elevate this concept by promoting a structured state-first approach, thereby minimizing the requirement for convoluted logic.

This post marks the first installment of a four-part series, encompassing everything you need to begin constructing frontend applications using the robust principles of finite state machines. We've established the groundwork for comprehending FSMs in this article and will delve deeper in the next. Subsequent articles will concentrate on leveraging XState, a finite state machines library for JavaScript runtimes, for developing frontend applications.

If you found this article informative and are intrigued by the FSMs pattern in JavaScript, keep an eye out for the upcoming articles in this series. Stay tuned for more insights and practical applications!

state Article's
30 articles in total
Favicon
Svelte 5: Share state between components (for Dummies)
Favicon
Pampanga State Agricultural University
Favicon
Data Flow in LLM Applications: Building Reliable Context Management Systems
Favicon
Props and State in React
Favicon
Radar Market Innovations: Phased Array Solid-State Radar Development
Favicon
A single state for Loading/Success/Error in NgRx
Favicon
Advanced State Management - XState
Favicon
Top 7 Tips for Managing State in JavaScript Applications 🌟
Favicon
MithrilJS component with state management
Favicon
React State Management: When & Where add your states?
Favicon
STATE MANAGEMENT IN REACT
Favicon
State Management with Zustand
Favicon
A practical summary of React State variables & Props!
Favicon
State in React
Favicon
Weak memoization in Javascript
Favicon
Crafting a Global State Hook in React
Favicon
Reusing state management: HOC vs Hook
Favicon
State Vs Prop in React [Tabular Difference]
Favicon
Mastering XState Fundamentals: A React-powered Guide
Favicon
Does limiting state matter on the FrontEnd?
Favicon
Reducer vs. Finite State Machines: Understanding the Paradigm Shift
Favicon
A tool that can be used by anyone to manage React Query state externally
Favicon
Taming the State Beast: Redux vs. Recoil in React
Favicon
11 friends of state management in Angular
Favicon
React State Management
Favicon
How Can State Management Be Applied To A Real World Case-Scenario
Favicon
No more State Management with Signals
Favicon
How to keep state between page refreshes in React
Favicon
How to sync React state across tabs with workers
Favicon
State Management Patterns in React

Featured ones: