loading...
Cover image for Throw Out Your React State-Management Tools

Throw Out Your React State-Management Tools

bytebodger profile image Adam Nathaniel Davis Updated on ・17 min read

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.

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

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

  4. 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:

     <App>
          ↓
  <TopTier>
          ↓
<MiddleTier>
          ↓
<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)

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.

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

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

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

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

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)

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)

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.

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

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

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

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

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

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

   // 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 theContextApiIsCoolvariable, 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:

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.

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

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

Posted on by:

bytebodger profile

Adam Nathaniel Davis

@bytebodger

React acolyte, jack-of-all-(programming)trades, full-stack developer

Discussion

markdown guide
 

Overmind.js - the redux that should have been written in the first place.

It's not forcing an overly complex "functional" style with all its unnecessary wrapping a function in a function. You don't need 5 libraries just to be able to do async events. It just works as you expect it.

A random thought: there is a lot of hatred for OOP in JS circles - rightly so, since the language was not built for that (and neither for FP). However the idea of having private vs. public fields is excellent and it is connected to global states: if you put any minuscule implementation detail into it, people are going to depend on them making the application hard to change (but it would be still "theoretically correct").

 

Interesting. I haven't been exposed to Overmind before, but I'll definitely check it out!

 

Thanks for another interesting post. I wonder, does 'logToConsole' and the other 'methods' belong in the App state?
I have never used the context API but it looks like you could have stored them in the context outside of the whole 'state management' system, right?

Regarding

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.

I fully agree with the sentiment. This is a mistake the React team keeps repeating for some reason (most recently with Hooks).

 

Thanks for the feedback! As for the location of the methods in <App>... yeah, I get what you're saying. Whenever I'm writing a "real" app that doesn't exist purely for demo reasons, I never put any real logic (or methods, or state) in <App>. To me, it's a kinda "best practice" to treat that initial "springboard" component as a completely blank container whose sole purpose is just to launch the rest of the app. No one's ever explicitly told me that this should be done as a "rule". It's just something that I feel has kinda become understood outta (too many) years of experience.

For me, a salient analogy would be Java's public static void main() method. Every Java app must have it. And yet, it's kinda an amateur practice to shove a buncha logic into it. (In fact, I think it's kinda amateurish to put any real logic into it.) It's only "real" function, is just to launch the rest of the app. When I create React apps (for me, or for employers) that I expect to actually be deployed into a proper production setting, I always treat <App> like Java's public static void main().

The only time I break this "rule" is when I'm throwing something up purely for demo purposes (e.g., like on StackBlitz).

 

I get you point. What I meant is that usually methods that can't change don't belong in 'state'. They are not part of anyone's state :)
They can still be part of the context though (I guess).
It is a minor point and not directly related to what you were trying to illustrate. It just popped to me as I was scanning through the code.

OK, I think I see what you're getting at. I will freely admit that I'm still figuring out all the ins-and-outs of the Context API. But I found, in my initial setup, that I had to put references to those functions into the associated component's state for the whole context-reference thing to work properly.

Now... am I stating that as an "absolute truth" that must be done to make the Context API work?? Definitely not. I've been doing dev for 20+ years. But I literally started experimenting with the Context API yesterday. So it's perfectly possible that I didn't actually need to stuff those function references into state to make the whole thing work. That's just the way that my solution "ended up" when I finally got it all working.

(Side note: I will freely admit that, in some respects, the Context API was a little bit challenging for me to "grok". I actually played around - with a dozen-or-more approaches - before settling on the example that I put in the demo.)

 

Continuing that thought, if this were a "real" app, most/all of that state/functionality would be shunted down into <TopTier>. That probably woulda been more effective for the demo, cuz the whole point was just to show, "Hey... See? We're skipping the <MiddleTier> component and porting values/functions directly into <BottomTier>."

Of course, if this were a "real" app, there would be no logToConsole() function. No self-respecting dev would create a wrapper function whose sole purpose is to perform a console.log(). My only purpose in including that (silly) little function was to demonstrate that, "See! We're calling an <App> function directly from <BottomTier> - and we're doing it solely through a Context API reference."

Now, as for the toggleTheContextApiIsCool() function, there is a specific reason why it was placed in the <App> component. I can easily accept the idea that the theContextApiIsCool state variable should possibly "live" in the <TopTier> component. But one of the (lesser) points of my post is that, IMHO, if we have stateVariableX that "lives" in <ComponentX>, then, whenever possible, the functions that update that state variable should also "live" in <ComponentX>.

So to put this another way, in this example, I don't particularly care where the state variable of theContextApiIsCool resides. But wherever you choose to place it, the subsequent function(s) that call setState() on that value should also reside in the same component.

 

I agree with where state should live. I was saying a function is not state (unless it is dynamically changing). Again, purely semantic point.

Yeah. I get that. And I think I answered that in my other answer to your comment. To be frank, the only part of my proposed Context API solution that feels a little "weird" or... "off" is the stuffing of those function references into state. I'm not sure if I had to do it that way. But that seemed to be the way that I "had to" do it to get my demo to compile/work. When I tried it without the state-function references, it broke when I tried to invoke a context-based function that resolved to a function that invoked setState().

(Does that make sense?? It's fairly clear in my mind at the moment - but it's one of those concepts that's kinda confusing to try to spell out in text.)

That being said, once I got it working, I didn't necessarily dislike it. On some level, it's almost kinda cool. Cuz, aside from trying to find a better approach to (what is, IMHO) the bloated mess of Redux, I kinda liked the idea that we can (or... have to) explicitly define the functions that are available via the Context API.

I dunno... I could probably be convinced that it's somehow "sub-optimal". Or that it's somehow an "anti-pattern". But, for the time being, it kinda seems like a good thing.

Thanks for all of the replies. We were just replying to one another too quickly so I missed your previous one :)
Maybe I will play with it a bit too (will try to make time). As I probably expressed before, one of my main issues with React (some other frameworks have that too) is its insistence that everything is 'ui related state' and rendering logic. I find this to be thoroughly lacking.

 

As for the whole "rendering cycle problem", I'm glad that you understand my point. Lemme give you a more tactical "real world" example that I ran into recently.

I had to build a React app with backend/API connections. And I was specifically requested to craft those API connections via GraphQL. To be frank, I'd read about GraphQL before, but I hadn't actually implemented GraphQL endpoints before. (Obviously, I'm all-too-familiar with REST.)

So, after doing some basic get-up-to-speed reading, I did what a lotta React devs do - I started playing with some of the major React/GraphQL libraries that I could leverage on NPM. I wasn't using some esoteric packages that have minuscule downloads. I was trying to use the "official", "approved" React/GraphQL libraries. The ones that are either written, or explicitly endorsed, by the core GraphQL team.

But I kept running into one core problem that bugged the hell outta me. All of the "official" libraries wanted me to embed/invoke the GraphQL components directly in the render() function.

Once I got everything properly loaded up and connected, I noticed that the damn components were constantly calling my backend API - sometimes, two or three times - for a single load of the application.

To be honest, I don't think the client woulda really given a shit. But it bugged me greatly. I HATE invoking unnecessary API calls. And when I'm doing an SPA, I take great pride in trying to minimize any unneeded renders and/or API calls.

Of course, one of the "hallmarks" of React development is the (near constant) struggle to halt unnecessary renders. So, at first, I attacked it as a "stop the unnecessary renders" problem. But no matter what I was doing, I just couldn't get it to where it would only call the backend API once. It always called the API at least twice.

Finally... I told myself that this was just stupid/wasted effort. I mean, in the end, a GraphQL query (or a REST query, or a SOAP query, or any kinda query) is just text that's gotta be formatted in a very-specific way. So I ended up just ripping out the React/GraphQL libraries and writing the queries manually - in a fetch() - that didn't "live" in the render() function.

Once I did it the "manual" way, it worked beautifully. It called my backend API no more than the exact number of times that were necessary to provide the needed functionality.

And yet... soooo many React libraries just assume that you're gonna invoke all their custom components in the render() function.

Sigh...

 

Thanks for sharing. I didn't use graphQL yet (beyond playing around in a sandbox) and didn't know the problem extends to it. I love the fact you didn't just accept it and went back to basics:)
I think one major thing a lot of framework users don't understand is that the people who make these frameworks are just normal developers that don't like to other people's frameworks (so they make their own) :)

 

I had developed a pertty complex app without any state management tool, with Context API. If there are any downfalls where components are getting rerendered, we can definitely optimize that. We can surely architecture state with context API like there is no need of external tool to do that

 

I agree. And that kinda brings us full circle back to the original point of this particular post. The typical flow seems to go like this:

  1. Team wants to share state.
  2. Team creates global store.
  3. Team starts to realize all the headaches of unplanned use.
  4. Team starts bolting a ton of extra controls on top of the global store to avoid these headaches.
  5. The team's code is now a Frankensteined aberration of what they originally set out to build.

But this is why I'm excited about the Context API. By using that, you can keep full control of that value in its original component. No need to route all changes through a series of actions or reducers. In other words, you can let React's native setState() handle all state updates - just as it was originally designed to do.

It is true that, with the Context API, those value do have to be "exposed". But that should usually be quite painless. In the case of state variables, if you're passing all of state into the ContextProvider's value, there's no need to worry about making future state variables accessible. It's already done.

 

Have you run into rerendering issues with passing functions that update state via Context? I think one solution is to memoize the functions, but I'm still experimenting with that.

 

I'm playing around with something today and your question came to mind. If you want the context to be properly updated on state changes, you must be referencing it from within the render() function. I'm gonna do a follow-up post on this soon that will illustrate this. This might be part of the issue you've experienced.

 

And... here it is:

dev.to/bytebodger/a-context-api-fr...

It's not radically different from what I already highlighted. But it emphasizes the need to put the context references in the render() function. It also provides a few stylistic improvements to the first post...

 

No, I haven't run into any issues with that. I haven't used it enough yet to say that there are no issues with it. But my initial tests haven't revealed any problems... so far.

 

I Love this improvement of state sharing while I prefer to keep even react out of the way. I would always prefer modules for shared state as you can dynamic import them and this way share state on module level inside components.

i would love to hear your feedback about this dev.to/frankdspeed/the-html-compon...

 

Thanks for sharing but,WOW. That was a lot of text. Probably a lot of the words could be removed and the idea would still come across just as clear if not more.