The Splintering Effects of Redux

bytebodger

Adam Nathaniel Davis

Posted on March 8, 2020

The Splintering Effects of Redux

I love me some React. It's become my go-to framework-of-choice for nearly any new dev project. And my professional life is spent as a mostly-React-centric developer. But... I also feel that side-effects of React's state-management... challenges, coupled with a growing elitism in the React/JavaScript community, has led to a beautiful framework becoming increasingly splintered.

Allow me to explain...

In the Beginning

On the eighth day, The Creator (Jordan Walke) created React. And it was good. But almost from the beginning, there was something rotten festering in the Garden of Eden (React). This rotten apple was the "core" method of handling shared state management.

Specifically, the base/core/out-of-the-box implementation of React specified that shared values would be passed between components through props. This is (un)affectionately referred to by React devs as prop drilling - the concept that values are "shared" between components by constantly passing them down, from one layer through another and yet another and another (ad nauseum), until some lower-level component finally has access to the raw values (state) or callbacks (functions) that it needs to serve its core function.

Most seasoned devs could read the original spec and think:

Mannnn... that will never work for the Crazy Complex Legacy Apps that I'm dealing with.

So... mere seconds after "The Beginning", anyone who started evaluating React began devising "better" ways to share state between components.

The Prophet (Dan Abramov et. al.) was not oblivious to these concerns. So even as other developers were trying to develop their own global-state-management solutions, The Prophet gave us: Redux. And it was... good?

Umm... maybe. Maybe not.

But we're getting ahead of ourselves.

The Dark Ages of MVC

I can't attest as to why you might've gotten into React development. But I can absolutely remember what excited me about the framework. I saw React as a gorgeous way to circumvent the aging beast known as MVC.

For any devs "of a certain age", we can clearly remember a time when you couldn't escape MVC. Hell... you couldn't even get a job - any dev job - unless you mentioned "MVC" at least a dozen times during your tech interview, and took every opportunity to praise it.

MVC is no longer the tech flavor du jour. But I feel like its ghost still stalks modern dev teams. Its goals are still prevalent in any "mature" dev effort today. Because MVC was a major movement that aimed to codify seperation of concerns.

If you ever worked in an old-school server-side language that used no MVC, you understand the benefits of the pattern. Really old PHP, Perl, VB, or JSP apps would often have a single page of friggin code that would, in one fell swoop, try to do everything that was needed to render that page. In that single page, you could have HTML output (the View), database queries (the Model), and business logic that would determine which bits to show to the user at any given point (the Controller).

So back when any "real" programming was done on the server side, MVC was a useful pattern. You had anything drawn from the data layer (the Model), anything that was sent to the browser (the View), and any business logic that drove what the user should-or-should-not see (the Controller).

And this all made a lot of sense... when the browser was just a dumb client that was rendering whatever was sent down the pipe from the web server. But then JavaScript had its breakthrough - and all hell broke loose.

jQuery Distinguished Between Business Logic and Display Logic

Let's be absolutely clear: There's nothing in jQuery that's inherently MVC. But soooo many MVC apps (before jQuery) tried to treat everything that was sent to the browser as simple, static display. There was no distinction between business logic and display logic. Under the MVC model, if there was any "logic" to be applied to the page, that logic was supposed to live in the controller (which probably lived somewhere on the server).

But jQuery challenged that assumption (in a big way). Because then, for the first time, you could write a rich, client-side app with all sorts of fancy "logic" that was completely contained in the display layer (the View).

I can't speak for anyone else, but I'll admit that this is the first time I started thinking deeply about the difference between business logic and display logic. Because, in a "standard" MVC paradigm, all of that logic gets shoved into the Controller (which probably resides on the server). But as client-side applications finally started coming of age, the line between these two types of logic started to blur. And as they blurred, it became apparent that jQuery wasn't inherently equipped to handle this split.

The Interim Step of Knockout

React was hardly the first JS framework to provide rich, frontend capabilities that would update the DOM (the View) in real-time. In fact, the next "leap" forward from jQuery was, IMHO, Knockout. Knockout provided a "magical" feature known as two-way data-binding. Using Knockout, you could set a variable in one place, then you could update the value in many different places, and the UI would "auto-magically" update based upon the new value.

Knockout has, for the most part, fallen by the wayside. The idea of two-way data-binding has become something of a dirty word amongst many JS devs. I'll get into this in more detail further down in this post. But, for the time being, just put a bookmark on this idea as we move along in JS's evolution...

React to the Rescue

When I first saw React, it legitimately excited me! It provided an oh-so-elegant model whereby a dev could define all of the display logic that accompanied a given element (i.e., a component). In its "core" implementation, it was very obvious (to me) where any of a component's display logic should "live" - right inside the component itself.

Consider the following example:

import React from 'react';

export default class IdealImplementation extends React.Component {
   this.state = { value : '' };

   render = () => {
      return (
         <>
            <div>Type something in this field:</div>
            <input
               onChange={this.updateTextField}
               name={'demoField'}
               value={this.state.value}
            />
         </>
      );
   };

   updateTextField = (event = {}) => {
      const newValue = event.currentTarget.value;
      this.setState({value : newValue});
   };
}
Enter fullscreen mode Exit fullscreen mode

In the React ecosystem, it doesn't get much more basic than this. We have a dirt-simple component that has a basic implementation of an <input> field. The value of that <input> field is driven by its state.

I gotta tell you that, as a self-professed "old-school" developer, this just makes sooooo much sense to me. The "memory" of the <input> field - i.e., its state - is saved right in the component itself.

We're not calling back to the server to inquire about the <input> field's state. We're not dependent upon a new roundtrip call to the web server to tell us how the <input> field should be rendered. It's all managed right here in the display component that's handling (rendering) this component. IN the display!

Should we be calling out to the web server (or, to another component) to inquire as to how this component should be rendered??? Of course not. That would represent a ridiculous "separation of concerns". The server shouldn't be telling this <input> field how to render/act. Because this <input> field is inherently a display component. This means that any "logic" that drives its presentation is, inherently, display logic. So the logic that tells us how to display this component should be housed - wait for it... right here, IN this component.

But the common sense of this approach doesn't stop here. This component does have some small degree of state. This component has to "remember" something about itself. Specifically, it has to "remember" what values have already been typed into the <input> field.

So where should that state be stored? Well... how about, right here, inside the component that houses the <input> field itself???

When I type it out this way, it seems painfully obvious. The display logic for this <input> field should be housed right here, where the <input> field is rendered. And what if the user actually interacts with this field (by typing inside of it)? Well... once again, that small bit of state should also be housed right here, where the <input> field is rendered. If we need to update this value, we should be doing that update right here, where the <input> field is rendered, via setState().

The Splintering of Global State Management

Maybe you're nodding along with me and wondering what is the point of this whole post? React provides a beautiful, native way for components to maintain their own "memory" (state). And it provides a beautiful, native way for us to update those values with setState().

So... what's the problem???

The "problem" comes when we decide that we want to share the value of this <input> field out to other components. React does provide a native means to accomplish this - by passing the value down to descendant components via props. But... most professional React devs have come to see this process as unmanageable and un-scaleable in "large-scale" React applications. In fact, they even created a derogatory term for it: prop drilling.

Let's imagine that we have 20 nested, downstream components that all need to "listen" to the value of this <input> field as the user types a value into it. Under React's "core" implementation, this would mean that we'd have to pass the value of the <input> field down, via props, through 20 layers of descendant components.

Most of the professional React devs that I know would consider this situation to be unmanageable using "base" React functionality. The idea of passing a single value, via props, through 20 nested layers, is the kind of challenge that would lead most devs to reach for a global state-management solution.

A "Solution" With a Whole Host of New Problems

The prop drilling problem is why React devs use a global state-management solution. There are many of them out there, but the "default" choice is Redux. Because Redux was crafted by devs who are closely aligned with the team that wrote React.

In this example, if the <input> value must be shared out to many other components, most React devs assume they must use a global state-management solution. And this is usually: Redux. Under this scenario, they put the <input> field's value into the Redux global store. Then they can be confident that this same value will be available to any other components that need it - without any of the hassle that's inherent in prop drilling.

So if the <input> field's value must be shared out to many other components, the "default" answer is to shove the field's state value into a global state-management store. But this is not without side effects...

Remember, above, where I talked about Knockout? Many JS devs grew weary of that library because they had a global variable that was set in one place, but it could be updated in many places. So when they found that their variable somehow ended up with a "wrong" value, it was unduly difficult to trace the origination of that bug, because it was a pain to figure out exactly where the aberrant value had originated.

Redux Solves a Problem... By Creating Another Problem

The Redux creators knew about the headaches that could arise from true two-way data binding. To their credit, they didn't want to recreate that problem. They understood that, if you put a variable into a global store, then, in theory, any other component with access to the global store can update that variable. And if any component accessing the global store can update the variable, you run into the same headaches experienced with Knockout, whereby it can be extremely difficult to track the source of your bug.

So they implemented a framework that requires you to update your global variables through a host of ancillary functions. There are reducers and actions and subscribers. All of these ancillary constructs are designed to "control" the way that global variables are updated - and to provide single points where any side-effects can be generated.

But the practical effect of these changes is that we export a huge amount of our display logic out into far-flung files/directories. When you look inside a project that's deeply ingrained in Redux, it can be very confusing to figure out exactly where the changes are being made.

You can have a basic state variable foo that is defined in <SomeComponent>. But when you open <SomeComponent>, there is little-or-no code in that component that drives the value (or the side effects) of foo.

For this reason, I've found that not only is Redux development a separate skill in itself, but merely troubleshooting Redux apps is also its own separate skill. It's not sufficient to merely be "a React expert". If you don't have significant experience troubleshooting Redux apps, it can take farrrrr longer than a "Redux developer" to find even the simplest of bugs.

A Better Way Forward

I won't rehash my previous posts here, but if you look at the other entries in this series, you'll see that I've been spending a lot of time with the Context API. This hasn't been mere intellectual curiosity. I've been doing this because I find Redux's core framework to be an unnecessary abstraction.

When you use the Context API (or React's "default" prop drilling approach), it preserves soooo much of React's original beauty. Via the Context API, I can share functions/variables out to the rest of my app. But... I can also ensure that those variables are only ever updated in a single place. Specifically, I can keep the state of any component - big or small - confined to the original component where that state "lives". And I can do it all with React's native setState() approach.

When I'm using the Context API, I can open a component and see whatever state variables are defined for that component - right there, in that component. If those variables ever need to be updated, I can also see all of the functions/side-effects that affect those variables - right there, in that component.

I don't have to trace logic through far-flung actions and reducers that live in scantly-associated directories. If a component has a state variable of foo, then I can confidently open that single, simple component to see all of the ways in which foo can theoretically be changed. I can pass functions that allow other components to update foo, but the actual work of updating foo is always done in one, logical place - in the same component where foo was originally defined.

💖 💪 🙅 🚩
bytebodger
Adam Nathaniel Davis

Posted on March 8, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related