Throw Out Your React State-Management Tools
Tags: react, redux, context, codequality
Published: 2020-02-23 03:28:16

A few days ago, I wrote a post about a workaround/hack that I've been using in React to pass around components' state variables and functions.  I knew that my approach was by-no-means perfect, so I openly solicited feedback from the community - and they delivered.  

What I'm going to discuss/illustrate here is (IMHO) a far better approach to shared state in React.  This approach does _not_ use any third-party or bolt-on state-management libraries.  It uses React's core constructs to address the "challenge" of prop drilling.  Specifically, I'm talking about React's Context API.

## Some Context on the Context API

The Context API's been available in React for a long time.  However, until about 18 months ago (when React 16.3 was released), the Context API was listed as "experimental".  It was bundled in the core React library, but there were expected changes in the syntax that weren't solidified until version 16.3.

Because of that scary "experimental" tag, and because, quite frankly, I found the previous documentation to be somewhat obtuse, I never really paid _too much_ attention to the Context API.  I knew it was there, but any time I tried to really leverage it, it just didn't seem to be working the way that I wanted it to.  

But my previous post - which contained a lot of angst about the elitist React dogma that surrounds Redux - got me to reassess the Context API.

In full disclosure, there's also been some prominent discussion that the Context API is not appropriate for "high-frequency updates".  Personally, I think that's a pile of BS (and I'll explain why below).  But it's worth noting that some people would use this as a reason to dismiss the Context API as a valid solution (or as a reason to cling to their beloved Redux).

## Features of the Context API

1. It's no longer _experimental_.  It's been available for years, but it's now graduated to the "big leagues".  This is important to note because the syntax _did in fact change_ between the "experimental" and "official" versions.

1. It's part of core React.  So there's no need to tack on a pile of additional packages to manage/share your state.

1. It has a minimal footprint.  In the examples I'll show below, you'll see that you can leverage the Context API with _very few_ extra lines of code.  This is in stark contrast to Redux, which is known (even amongst its biggest fanboys) to require a _massive_ amount of "boilerplate" code.

1. It can be used in a very efficient, targeted manner.  Just like _any_ solution for passing/sharing state values, it's _possible_ to muck the whole system up by creating a monolithic (and gargantuan) **Global Store** that will drag your application to its knees.  But this is easily avoidable with the Context API with a modicum of architectural planning.  You can also choose, in a very targeted fashion, _which_ values are stored, at what _level_ of the application they're stored, and _which_ descendant components have access to the Context values.  In other words, you don't have to put **_All The Things!!!_** in the Context store.  And once something is stored in a Context store, it doesn't have to be available to **_All The Things!!!_**.

## The Problem

The biggest thing that I'll be addressing here is called _prop drilling_.  It's the idea that, in a "base" React implementation, you probably have a hierarchy of components.  Each component can have its own values (i.e., its own _state_).  If a component at the bottom of the hierarchy tree needs access to something from the top of that same tree, the default React solution is to pass those values - via _props_ - down to the bottom component.  

But a potential headache arises if there are many layers between the higher-level component which holds the desired value, and the bottom-level component which needs access to that value.  If, for example, there are 100 components "between" the higher-level component and the bottom-level component, then the required values would have to be passed through each of those 100 intermediary components.  That process is referred to as _prop drilling_.

In most React shops, the answer has been to reach for a state-management solution to bolt onto the application.  The most common solution has been Redux, but there are many others.  These tools create a shared cache that can then be accessed by any component in the app, allowing devs to bypass the whole prop drilling "problem".  Of course, there are many potential problems that can be _introduced_ by state-management libraries, but that's a topic for another post...

## The Setup

Let me start by saying that this post isn't going to show you some radically-new, previously-undiscovered technique.  As stated above, the Context API's been available in experimental mode for many years.  Even the "official" version was solidified with React 16.3, which came out ~18 months ago (from the time that this was written).  

Furthermore, I'll gladly admit that I gained clarity and inspiration from several other posts (at least one of them was right here on `DEV`) that purport to show you how to use the Context API.  I'm not reinventing any wheels here.  And I don't claim to be showing you anything that you couldn't _grok_ on your own by googling through the official React docs and the (many) tutorials that are already out there.  I'm only doing this post because:

1. This blog is basically my own, free, self-administered therapy.  And it helps _me_ to codify my thoughts by putting them into a (hopefully) coherent document.

2. There are a few _small_ details of my preferred implementation that are probably _a little_ unique, compared to the other demos you might see.

3. This post (and the approach I'm about to outline) is a direct follow up to my previous post titled _"Why Is This An 'Anti-Pattern' in React???"_

So with all of that in mind, imagine that we have a very basic little React application.  Even modest applications tend to employ some kind of component hierarchy.  So our application will look like this:

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`<App>`
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&darr;
&nbsp;&nbsp;`<TopTier>`
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&darr;
`<MiddleTier>`
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&darr;
`<BottomTier>`

Remember: The central "problem" that we're trying to solve is in regard to _prop drilling_.  In other words, if there is a value/function that resides in the `<App>` component, or in the `<TopTier>` component, how do we get it down to `<BottomTier>`?

(Of course, you may be thinking, "For an app that's _this_ small, it would be better practice to simply pass the value/function down through the hierarchy with props."  And, for the most part, you'd be _right_.  But this is just a demo meant to illustrate an approach that could be done on much larger apps.  In "real" apps, it's easy for the hierarchy to contains many _dozens_ of layers.)

In the past, if a developer didn't want to pass everything down through props, they'd almost always reach for a state-management tool like Redux.  They'd throw all the values into the Redux store, and then access them as-needed from any layer of the hierarchy.  That's all fine-and-good.  It... _works_.  But compared to what I'm about to show you, it's the equivalent of building a sandcastle - with a bulldozer.  

Here's the code for all four of the components in my demo app:

### `<App>` (App.js)

```javascript
import React from 'react';
import TopTier from './components/top.tier';

export const AppContext = React.createContext({});

export default class App extends React.Component {
   constructor(props) {
      super(props);
      this.state = {
         logToConsole: this.logToConsole,
         myName: 'Adam',
         theContextApiIsCool: true,
         toggleTheContextApiIsCool: this.toggleTheContextApiIsCool,
      };
   }
   
   logToConsole = (value) => {
      console.log(value);
   };
   
   render = () => {
      return (
         <AppContext.Provider value={this.state}>
            <TopTier/>
         </AppContext.Provider>
      );
   };
   
   toggleTheContextApiIsCool = () => {
      this.setState((previousState) => {
         return {theContextApiIsCool: !previousState.theContextApiIsCool};
      });
   };
}
```

Nothing too exotic here.  For the most part, it looks like any "normal" `<App>` component that could be launching nearly any kind of "industry standard" React application.  There are only a few small exceptions:

1. Before the class declaration, we're creating a constant that's a new instance of React's built-in context handler.  The new context _will be specific to the `<App>` component_.

1. Notice that I didn't _name_ the context something like `GlobalContext` or `SharedState`, because I don't want this context to hold **all** the state values for the whole damn application.  I only want this context to refer, very specifically, to the values that are resident on the `<App>` component.  This will be critical later when I discuss performance (rendering) considerations.

1. Aside from housing some basic scalar values, the `state` object also has references to the component's functions.  This is critical if we want components further down the hierarchy to be able to _call_ those functions.

1. Before the `render()` function calls `<TopTier>`, that component is wrapped in `<AppContext.Provider>`.  

### `<TopTier>` (/components/top.tier.js)

```javascript
import MiddleTier from './middle.tier';
import React from 'react';

export const TopTierContext = React.createContext({});

export default class TopTier extends React.Component {
   constructor(props) {
      super(props);
      this.state = {currentUserId: 42};
   }
   
   render = () => {
      return (
         <TopTierContext.Provider value={this.state}>
            <div style={{border: '1px solid green', margin: 20, padding: 20}}>
               This is the top tier.
               <MiddleTier/>
            </div>
         </TopTierContext.Provider>
      );
   };
}
```

This is similar to the `<App>` component.  First, we're creating a context _that's specific to the `<TopTier>` component_.  Then we're wrapping the `render()` output in `<TopTierContext.Provider>`.

### `<MiddleTier>` (/components/middle.tier.js)

```javascript
import BottomTier from './bottom.tier';
import React from 'react';

export default class MiddleTier extends React.Component {
   render = () => {
      return (
         <div style={{border: '1px solid green', margin: 20, padding: 20}}>
            This is the middle tier.
            <BottomTier/>
         </div>
      );
   };
}
```

This is the last time we'll be looking at this component.  For the purpose of this demo, its only real "function" is to be skipped over.  We're gonna show that, with the Context API, we can get the values from `<App>` and `<TopTier>` down to `<BottomTier>` without having to explicitly pass them down the hierarchy through props.

### `<BottomTier>` (/components/bottom.tier.js)

```javascript
import React from 'react';
import {AppContext} from '../App';
import {TopTierContext} from './top.tier';

export default class BottomTier extends React.Component {
   render = () => {
      const {_currentValue: app} = AppContext.Consumer;
      const {_currentValue: topTier} = TopTierContext.Consumer;
      app.logToConsole('it works');
      return (
         <div style={{border: '1px solid green', margin: 20, padding: 20}}>
            <div>This is the bottom tier.</div>
            <div>My name is {app.myName}</div>
            <div>Current User ID is {topTier.currentUserId}</div>
            <div style={{display: app.theContextApiIsCool ? 'none' : 'inherit'}}>
               The Context API is NOT cool
            </div>
            <div style={{display: app.theContextApiIsCool ? 'inherit' : 'none'}}>
               The Context API is cool
            </div>
            <button onClick={() => app.toggleTheContextApiIsCool()}>
               Toggle `theContextApiIsCool`
            </button>
         </div>
      );
   };
}
```

OK... there's some fun stuff happening in this component:

1. We import references to `AppContext` and `TopTierContext`, because we'll want to leverage variables/functions that reside in those components.

1. We destructure `_currentValue` out of `AppContext.Consumer` and `TopTierContext.Consumer`.  This allows us to grab the values from those contexts with an imperative syntax.

1. Before the `render()` returns anything, we directly invoke `app.logToConsole()`.  This demonstrates that we can directly call functions that "live" in the `<App>` component.

1. Inside the `return`, we access a state variable directly from `<App>` when we display `{app.myName}`.

1. On the next line, we access a state variable directly from `<TopTier>` when we display `{topTier.currentUserId}`.

1. The next two `<div>`s will dynamically display-or-hide a message based on `<App>`'s `theContextApiIsCool` state variable.

1. Finally, we show the user a button that allows them to toggle the state variable `theContextApiIsCool` in the `<App>` component by calling `{app.toggleTheContextApiIsCool()}`.

If you'd like to see a live version of this, you can find it here:

https://stackblitz.com/edit/react-shared-state-via-context-api

## The "Gotcha's" and "Downsides" to This Approach

There are none!  It's a _flawless_ solution!!!

(Just kidding.  Well... sorta.)

**Global-vs.-Targeted State Storage**

When you first start reaching for state-management solutions, it's natural to think:

> I just want ONE state store (to bring them all, and in the darkness, bind them).

OK, _I get that_.  I _really_ do.  But I always chuckle a little inside (or directly in someone's face) when I hear them preach about avoiding needless dependencies in their apps - _and then they dump their favorite state-management tool into damn-near every component across their entire app_.  Repeat after me, people:

> Shared state-management tools are the **definition** of dependency injection.

If you want to proselytize to me all day about the dangers of entangling dependencies, then fine, we can have an intelligent conversation about that.  But if I look at your apps, and they've got a state-management tool littered throughout the vast majority of your components, then you've lost all credibility with me on the subject.  If you really care about entangling dependencies, _then stop littering your application with global state-management tools_.

There's absolutely a time and a place when state-management tools are a net-good.  But the problem is that a dev team decides to leverage a _global_ state-management solution, and then (_Shocking_!) they start using it _globally_.  This doesn't necessarily "break" your application, but it turns it into one, huge, tangled mess of dependencies.

In the approach I've outlined above, I'm using shared state-management (_via React's built-in Context API_) in a discrete-and-targeted way.  If a given component doesn't need to access shared state, it simply doesn't import the available contexts.  If a component's state never needs to be queried by a descendant, we never even bother to wrap that component's `render()` output in a context provider.  And even if the component _does_ need to access shared state, it has to import _the exact contexts_ that are appropriate for the values that it needs to perform its duties.

Of course, you're not required to implement the Context API in the manner I've outlined above.  You _could_ decide to have only **one** context - the `AppContext`, which lives on the `<App>` component, at the uppermost tier of the hierarchy.  If you approached it in this way, then `AppContext` would truly be a _global_ store in which all shared values are saved-and-queried.  I do **not** recommend this approach, but if you're dead-set on having a single, _global_, state-management solution with the Context API, you _could_ do it that way.

But, that approach could create some nasty performance issues...

**Performance Concerns During High-Frequency Updates**

If you used my approach from above to create a single, _global_ store for **_ALL_** state values, it could drive a sizable application to its knees.  _Why???_  Well, look carefully at the way that we're providing the `value` to the `<AppContext.Provider>`:

```javascript
   // from App.js
   render = () => {
      return (
         <AppContext.Provider value={this.state}>
            <TopTier/>
         </AppContext.Provider>
      );
   };
```

You see, `<AppContext.Provider>` is tied to `<App>`'s _state_.  So if we store **_ALL THE THINGS!!!_** in `<App>`'s state (essentially treating it as a _global_ store), then _the entire application will re-render any time any state value is updated_.  If you've done React development for more than a few minutes, you know that avoiding unnecessary re-renders is Item #1 at the top of your performance concerns.  When a React dev is trying to optimize his application, he's often spending most of his time hunting down and eliminating unnecessary re-renders.  So anything that causes _the entire damn application_ to re-render in rapid succession is an egregious performance flaw.

Let's imagine that `<BottomTier>` has a `<TextField>`.  The value of the `<TextField>` is tied to a state variable.  And every time the user types a character in that field, it requires an update to the state value upon which that `<TextField>` is based.  

Now let's imagine that, because the dev team wanted to use my proposed Context API solution as a single, _global_ store to hold **_ALL THE THINGS!!!_**, they've placed the state variable for that `<TextField>` in `<App>`'s state (even though the `<TextField>` "lives" at the very bottom of the hierarchy in `<BottomTier>`).  This would mean that, every single time the user typed any character into the `<TextField>`, _the entire application would end up being re-rendered_.  

(If I need to explain to you why this is bad, then please, stop reading right now.  Step away from the keyboard - and burn it.  Then go back to school for a nice, new, shiny degree in liberal arts.)

> So is _this_ the Achilles' Heel that invalidates any use of the Context API for shared state-management and sends us all running back to Redux??

Of course not.  But here's my (unqualified) advice:  If your little heart is dead-set on having **The One State Store To Rule Them All**, then... yeah, you should probably stick with your state-management package-of-choice.  

I reserve the right to update my opinion on this in the future, but for now, it _feels_ to me that, if you insist on dumping _all_ of your state variables into a single, _global_ state-management tool, then you should probably keep using a state-management package.  Redux, specifically, has deployed many optimizations to guard against superfluous re-renders during high-frequency updates.  So kudos to them for having a keen eye on performance (no, _really_ - a lotta people a lot smarter than me have poured copious hours into acid-proofing that tool).

But here's the thing:

> Why are you obsessed with the idea that state-management must be a global, all-or-nothing solution??

As I've already stated:

    globalStateManagement === massiveDependencyInjection

The _original_ idea of React was that state resides in the specific component where that state is used/controlled.  I feel that, in many respects, the React community has progressively drifted _away_ from this concept.  But... _it's not a bad concept_.  In fact, I would (obviously) argue that it's quite sound.

So in the example above, I would argue that the state variable that controls our proposed `<TextField>` value should "live" in the `<BottomTier>` component.  Don't go lifting it up into the upper tiers of the application where that state variable has no canonical purpose (or, we could say, no _context_).  

Better yet, create a wrapper component for `<TextField>` that will _only_ manage the state that's necessary to update the value when you type something into that field.  

If you do this, the Context API solution for shared state-management works beautifully.  Even in the demo app provided above, it's not too difficult to see that certain state values simply don't _belong_ in `AppContext`.  

A Boolean that indicates whether-or-not the user is logged in might comfortably _belong_ in `AppContext`.  After all, once you've logged in/out, there's a good chance that we need to re-render most-or-all of the app anyway.  But the state variable that controls the value of a `<TextField>` that exists, at the bottom of the hierarchy, in `<BottomTier>`???  That really has no business being managed through `AppContext`.

If it's not clear already, I believe that this "feature" of the Context API approach is _not_ a bug or a _flaw_.  It's _a feature_.  It keeps us from blindly dumping **_everything_** into some big, shared, global bucket.

**Tracking Down State Changes**

If you're using a state-management tool, you might be thinking:

> State variables can, theoretically, be updated from many different sources.  My Beloved State Management Tool allows me to ensure that those changes always pass through a single gateway.  And thus, my troubleshooting is easier and my bugs are less frequent.

In the demo I've provided, there are some concerns that might jump out at you.  Specifically, _any_ component that imports `AppContext`, in theory, has the ability to update the state variables in the `<App>` component.  For some, this invokes the nightmares that they might have had when troubleshooting in a framework that supported true two-way data binding.

> So if these state-altering hooks can be littered anywhere throughout the app, doesn't this Context API approach make my troubleshooting life hell??

Well... _it shouldn't_.

Let's look at the `toggleTheContextApiIsCool()` function in the `<App>` component.  Sure, it's theoretically possible that _any_ component could import `AppContext`, and thus, invoke a state change on `<App>`'s `theContextApiIsCool` variable.  

But the actual work of updating the state variable is only ever handled inside the `<App>` component.  So we won't always know _who_ invoked the change.  But we will always know _where_ the change took place.  

This is really no different than what happens in a state-management tool.  We import the references to the state-management tool (_anywhere_ in the application), and thus, _any_ component can, theoretically, update those state variables at will.  But _the actual update_ is only ever handled in one place.  (In the case of Redux, those places are called _reducers_ and _actions_.)  

Here's where I think that the Context API solution is actually _superior_.  Notice that, in my demo app, the `theContextApiIsCool` variable "lives" in the `<App>` component.  Any functions that update this value _also_ "live" in the `<App>` component.  

In this little demo, there is but a _single_ function with the ability to `setState()` on the `theContextApiIsCool` variable. Sure, if we want to _invoke_ that function, we can, theoretically, do it from _any_ descendant in the hierarchy (assuming that the descendant has already imported `AppContext`).  But the actual "work" of updating `theContextApiIsCool` all resides in the `<App>` component itself.  And if we feel the need to add more functions that can possibly `setState()` on the `theContextApiIsCool`variable, there is only one logical place for those functions to reside - _inside_ the `<App>` component.

What I'm talking about here is a component's _scope of control_.  Certain state variables should logically be scoped to the component where those variables are pertinent.  If a given state variable isn't pertinent to the given component, then that state variable shouldn't "live" in that component.  Furthermore, any function that alters/updates that state variable should only ever reside _in_ that component.

If that last paragraph gets your hackles up, it's because many state-management tools _violate_ this simple principle.  We create a state variable - and then we chunk it into the _global_ state-management store.  This, in effect, robs that variable of _context_.  

**Imperative-vs.-Declarative Syntax**

You might look at my demo app and feel a bit... _bothered_ by some of the syntax I've used.  Specifically, if we look at the `<BottomTier>` component, you may (as a "typical" React developer), be a _wee bit_ bothered by lines like these:

```javascript
const {_currentValue: app} = AppContext.Consumer;
const {_currentValue: topTier} = TopTierContext.Consumer;
app.logToConsole('it works');
```

_Please_... don't get too hung up on this syntax.  If you look at most of the Context API tutorials/demos on the web (including those on the React site itself), you'll quickly see that there are plenty of examples on how to invoke this functionality declaratively.  In fact, as far as I could tell, it looks as though damn-near _all_ of the tutorials feature the declarative syntax.  So don't dismiss this approach merely because I chose to toss in some "imperative voodoo".

I'm not going to try to highlight all of the declarative options for you in this post.  I trust your epic googling skills.  If you're wondering _why_ I chose this particular syntax, trust me:  I _love_ many aspects of React's inherent declarative ecosystem.  But sometimes I find this approach to be onerous. Here's my logic:

It seems that damn-near _every_ example I could find on Context API functionality (including those at https://reactjs.org/docs/context.html) seem to focus almost exclusively on the declarative syntax.  But the "problem" is that the declarative syntax is usually implicitly tied _to the `render()` process_.  But there are times when you want to leverage such functionality _without depending upon the rendering cycle_.  Also (and I admit that this is just a personal bias), I often feel it's "ugly" and difficult to follow when demonstrators start to chunk a whole bunch of basic JavaScript syntax _into the middle of their JSX_.

## So... Are You Ready To Throw Out Your State-Management Tools-of-Choice??

OK... I'll admit that maybe, _just possibly_, the title on this post is a _weeee bit_ "click-bait-y".  I don't imagine that any of you are going to go into work tomorrow morning and start yanking out all of your legacy state-management code.  But here are a few key seeds that I'd like to plant in your brain (if the narrative above hasn't already done so):

* The Context API can actually be pretty powerful.  I will raise my hand and admit that, as a React developer now for the last 4-or-so years, I really hadn't given it much serious consideration.  But now I'm starting to think that was a mistake.<br/><br/>

* State-management tools are _awesome_ tools - but I no longer believe they should be blindly implemented in all React codebases - and on all new React projects.  In fact... I'm starting to think that, in a perfect world, implementation of those tools would be _the exception_ - not _the rule_.<br/><br/>

* A monolithic, _global_ state store is, in many cases, a _lazy_ and sub-optimal solution.  Look... _I get it_.  I've been the first one to blindly assume that state-management is a must-have in any "serious" React application (even if my _strong_ preference has been for MobX, and **not** for Redux).  But my thinking is definitely evolving on this.  _Global_ stores are, essentially, dependency-generators.  And if you're _not_ going to insist upon a _global_ store, then why are you adamant about falling back on an additional set of libraries, when React's native Context API functionality might easily serve your purpose???

## So What Is _Your_ Verdict??

I'd _truly_ appreciate any feedback on this - positive _or_ negative.  What have I blatantly overlooked??  Why is Redux (or MobX, or _any_ state-management library) far superior to the Context API solution that I've proposed??

On one hand, I'll freely admit that I've written this post in a fairly-cocksure fashion.  Like I've discovered The One True Way - and all you idiots should just fall in line.

On the other hand, I'll humbly acknowledge that I didn't really start ruminating on this potential approach _until yesterday_.  So I'm glad for any of you to give me hell in the comments and point out all the stupid assumptions I've made.  Or to point out any of the horrific flaws in the Context API that I've either glossed over - or am totally unaware of.

I was wrong before.  Once.  Back in 1989.  Oh, man... _that_ was a horrible day.  But who knows??  Maybe I'm wrong again with this approach??  Lemme know...