A Context API Framework for React State Management
Tags: react, javascript, context, redux
Published: 2020-03-07 02:59:38

This is a follow up to my second post in this series:

https://dev.to/bytebodger/throw-out-your-react-state-management-tools-4cj0

In that post, I began digging into the Context API _in earnest_ for the first time in my experience as a React dev.  Since that post a few weeks ago, I'm glad to report that I've had a chance to dive into this in some detail and I've refined the ideas in the first post.

Although I've been employed professionally as a programmer for 20+ years, I still write the majority of my code _for free_.  In other words, I write thousands of LoC purely for myself.  I bring this up because I have a personal project that's currently sitting somewhere north of 30k LoC.  So I took my Context API findings and started applying them to this fairly robust codebase.

This has allowed me to assess the Context API in an environment that is much closer to "real-world apps" (and the stuff I'm building on the side definitely applies as _real world apps_).  I've honed the techniques in the original approach - and I can highlight a few "gotchas".

## Prelude

This post works from a few basic assumptions:

1. Most professional devs consider "prop drilling" to be an unmanageable solution for large-scale applications.

1. Most professional devs have come to see bolted-on state-management tools as a default _must have_.

1. The Context API is an interesting "dark horse" in the state-management arena because it's not an additional library.  It's _core_ React.  And the more I've investigated it, the more I'm convinced that it's incredibly flexible, robust, _and performant_.


## The Setup

I'm going to show a fairly-basic multi-layer app (but still more complex than most of the quick examples we see in many dev blogs).  There will be **no** prop drilling.  There will be **no** outside tools/packages/libraries used. I believe that what I'm about to illustrate is performant, fault-tolerant, and fairly easy to implement with no need for additional tools/packages/libraries.

I'm not going to outline `App.js`.  In my typical paradigm, there's no _real_ logic that ever goes in that file, and it's _only_ real purpose is to launch us into the application.  So please, just _assume_ that there's an `App.js` file at the top of this hierarchy.  

The rest of the files will be shown as a "tree" or "layered cake" structure that I typically use in my apps.  This proposed "framework" does not require this structure at all.  It's just the way that I tend to structure _my own_ apps and it works well to demonstrate shared-state amongst multiple layers of a codebase.

## contants.js

```javascript
import React from 'react';
import Utilities from 'components/utilities';

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

export default class Constants extends React.Component {
   constructor(props) {
      super(props);
      this.state = {
         apiUrl : 'http://127.0.0.1/',
         color : {
            blue : '#0000ff',
            green : '#00ff00',
            lightGrey : '#dddddd',
            red : '#ff0000',
         },
         siteName : 'DEV Context API Demo',
      };
   }

   render = () => {
      const {state} = this;
      return (
         <ConstantsContext.Provider value={state}>
            <Utilities/>
         </ConstantsContext.Provider>
      );
   };
}
```

**Notes:**

- Before the component is even defined, we're exporting a constant that will ultimately house _that component's context_.<br/><br/>

- "Context" can, technically, hold almost anything that we _want_ it to hold.  We can shove scalar values, or objects, or functions into the context.  Most importantly, we can transfer _state_ into context.  So, in this case, we put the whole of the component's state right into the context provider.  This is important because, if we pass _state_ into a prop, that means that the dependent component will update (re-render) if the underlying _state_ is updated.<br/><br/>

- Once we've done this, those same state values will be available anywhere in the descendant levels of the app _if we choose to make them available_.  So by wrapping this high level of the tree in `<Constants.Provider>`, we're essentially making these values available to the entire application.  That's why I'm illustrating the highest level in this hierarchy as a basic place in which we can store "global" constants.  This subverts a common pattern of using an `import` to make globals available to all downstream components.

## utilities.js

```javascript
import React from 'react';
import DataLayer from 'components/data.layer';
import {ConstantsContext} from 'components/constants';

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

let constant;

export default class Utilities extends React.Component {
   constructor(props) {
      super(props);
      this.sharedMethods = {
         callApi : this.callApi,
         translate : this.translate,
      };
   }

   callApi = (url = '') => {
      // do the API call
      const theUrlForTheApiToCall = constant.apiUrl;
      this.helperFunctionToCallApi();
      return theApiResult;
   };

   helperFunctionToCallApi = () => {
      // do the helper logic
      return someHelperValue;
   };

   translate = (valueToTranslate = '') => {
       // do the translation logic
       return theTranslatedValue;
   };

   render = () => {
      constant = ConstantsContext.Consumer['_currentValue'];
      const {state} = this;
      return (
         <UtilitiesContext.Provider value={this.sharedMethods}>
            <DataLayer/>
         </UtilitiesContext.Provider>
      );
   };
}
```

**Notes:**

- I've set up a bucket object in the `this` scope called `this.sharedMethods` that will hold references to any functions that I want to share down the hierarchy. This value is then passed into the `value` for `<Utilities.Provider>`.  This means that these functions will be available anywhere in the descendant components _where we chose to make them available_.<br/><br/>

- If you read the first post in this series (https://dev.to/bytebodger/throw-out-your-react-state-management-tools-4cj0), you might remember that I was dumping all of the function references _into state_.  For a lot of dev/React "purists", this can feel a little wonky.  So in this example, I created a separate bucket just to house the shared function references.<br/><br/>

- Obviously, I don't have to dump _all_ of the component's functions into `this.sharedMethods`.  I only put references there for functions that should specifically be called _by descendant components_.  That's why `this.sharedMethods` has no reference to `helperFunctionToCallApi()` - because that function should only be called from within the `<Utilities>` component.  There's no reason to grant direct access for that function to downstream components.  Another way to think about it is:  By excluding `helperFunctionToCallApi()` from the `this.sharedMethods` object, I've essentially preserved that function as being `private`.<br/><br/>

- Notice that the `value` for `<UtilitiesContext.Provider>` does **not** make any mention of `state`.  This is because the `<Utilities>` component has no state that we want to share to ancestor components.  (In fact, in this example, `<Utilities>` has no `state` whatsoever.  So there's no point in including it in the `value` for `<UtilitiesContext.Provider>`.)<br/><br/>

- Above the component definition, I've defined a simple `let` variable as `constant`.  Inside the `render()` function, I'm also setting that variable to the _context_ that was created for the `<Constants>` component.  You aren't **required** to define it in this way.  But by doing it this way, I don't constantly have to refer to the `<Constants>` context as `this.constant`.  By doing it this way, I can refer, anywhere in the component, to `constant.someConstantValue` and `constant` will be "global" to the entire component.<br/><br/>

- This is illustrated inside the `callApi()` function.  Notice that inside that function, I have this line: `const theUrlForTheApiToCall = constant.apiUrl;`.  What's happening here is that 1: `constant` was populated with the "constant" values during the render, 2: then the value of `constant.apiUrl` will resolve to `'http://127.0.0.1/` when the `callApi()` function is called.<br/><br/>

- It's important to note that `constant = ConstantsContext.Consumer['_currentValue']` is defined _in the `render()` function_.  If we want this context to be sensitive to future `state` changes, we must define the reference in the `render()` function.  If, instead, we defined `constant = ConstantsContext.Consumer['_currentValue']` in, say, _the constructor_, it would not update with future `state` changes.<br/><br/>

- This is not a "feature" of this framework, but by structuring the app this way, `<Constants>` becomes a global store of scalar variables, and `<Utilities>` becomes a global store of shared functions.

## data.layer.js

```javascript
import HomeModule from 'components/home.module';
import React from 'react';
import UserModule from 'components/user.module';
import {ConstantsContext} from 'components/constants';
import {UtilitiesContext} from 'components/utilities';

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

let constant, utility;

export default class DataLayer extends React.Component {
   constructor(props) {
      super(props);
      this.state = {
         isLoggedIn : false,
      };
      this.sharedMethods = {
         logIn : this.logIn,
      };
   }

   getModule = () => {
      const {state} = this;
      if (state.isLoggedIn)
         return <UserModule/>;
      return <HomeModule/>;
   };

   logIn = () => {
      // do the logIn logic
   };

   render = () => {
      constant = ConstantsContext.Consumer['_currentValue'];
      utility = UtilitiesContext.Consumer['_currentValue'];
      const {state} = this;
      return (
         <DataLayerContext.Provider value={{...this.sharedMethods, ...this.state}}>
            <div style={backgroundColor : constant.color.lightGrey}>
               {utility.translate('This is the Context API demo')}
            </div>
            {this.getModule()}
         </DataLayerContext .Provider>
      );
   };
}
```

**Notes:**

- The `backgroundColor` is picked up from the `<Constants>` context.<br/><br/>

- The text is translated using the `translate()` function from the `<Utilities>` context.<br/><br/>

- In this example, `this.sharedMethods` and `this.state` are spread into the value of `<DataLayerContext.Provider>`  Obviously, we're doing this because this components has both `state` variables _and_ `functions` that we want to share downstream.

## home.module.js

```javascript
import HomeModule from 'components/home.module';
import React from 'react';
import UserModule from 'components/user.module';
import {ConstantsContext} from 'components/constants';
import {UtilitiesContext} from 'components/utilities';

let constant, dataLayer, utility;

export default class HomeModule extends React.Component {
   render = () => {
      constant = ConstantsContext.Consumer['_currentValue'];
      dataLayer = DataLayerContext.Consumer['_currentValue'];
      utility = UtilitiesContext.Consumer['_currentValue'];
      return (
         <div style={backgroundColor : constant.color.red}>
            {utility.translate('You are not logged in.')}<br/>
            <button onClick={dataLayer.logIn}>
               {utility.translate('Click to Log In')}
            </button>
         </div>
      );
   };
}
```

**Notes:**

- The `backgroundColor` is picked up from the `<Constants>` context.<br/><br/>

- The `translate()` functions are picked up from the `<Utilities>` context.<br/><br/>

- The `onClick` function will trigger `logIn()` from the `<DataLayer>` context.<br/><br/>

- There is no reason to wrap this component's `render()` function in its own context provider, because there are no more children that will need `<HomeModule>`'s values.

## Visiblity/Traceability

From the examples above, there's one _key_ feature that I'd like to highlight.  Look at `home.module.js`.  Specifically, look inside the `render()` function at values like `constant.color.red`, `dataLayer.login`, or `utility.translate()`.  

One of the central headaches of _any_ global state-management solution is properly reading, tracing, and _understanding_ where any particular variable "comes from".  But in this "framework", I hope it's fairly obvious to you, even if you're just reading _a single line of code_, where something like `constant.color.red` comes from.  (Hint: It comes from the `<Constants>` component.)  `dataLayer.logIn` refers to a function that lives in... the `<DataLayer>` component.  `utility.translate` invokes a function that lives in... the `<Utilities>` component.  Even a first-year-dev should be able to just _read_ the code and figure that out.  It should be dead-simple-obvious as you browse the code.

Sure... you _could_ set `Constants.Consumer['_currentValue']` into some obtuse variable, like, `foo`.  But... _why would you do that_???  The "framework" that I'm suggesting here to implement the Context API implies that the _name_ of a given context variable also tells you exactly where that value _came from_.  IMHO, this is incredibly valuable when troubleshooting.

Also, although there's nothing in this approach to _enforce_ this idea, my concept is that:

> If a given context variable "lives" _in a given component_, then it's only ever updated _from that same component_.

So, in the example above, the `isLoggedIn` state variable "lives" in `<DataLayer>`.  This, in turn, means that any function that updates this variable should also "live" in `<DataLayer>`.  Using the Context API, we can pass/expose a function that will, ultimately, _update_ that `state` variable.  But the actual work of updating that `state` variable is only ever done _from within the `<DataLayer>` component_.  

This brings us back to the central `setState()` functionality that's been a part of core React from Day 1 - but has been splintered by the proliferation of bolt-on global state-management tools like Redux.  These tools suck that state-updating logic far away from the original component in which the value was first defined.


## Conclusions

Look... I totally understand that if you're an _established_ React dev working in _legacy_ codebases, you probably already have _existing_ state-management tools in place (probably, Redux).  And I don't pretend that anything you've seen in these little demo examples will inspire you to go back to your existing team and beg them to rip out the state-management tools.  

But I'm honestly struggling to figure out, with the Context API's _native_ React functionality, why you would continue to shove those state-management tools, by default, into all of your _future_ projects.  The Context API allows you to share state (or even, values that don't natively live in state - like, _functions_) _anywhere you want_ all down the hierarchy tree.  It's not some third-party NPM package that I've spun up.  It represents no additional dependencies.  And it's _performant_.

Although you can probably tell from my illustration that I'm enamored of this solution, here are a few things for you to keep in mind:

- The Context API is inherently tied to the `render()` cycle (meaning that it's tied into React's _native_ life cycle).  So if you are doing more "exotic" things with, say, `componentDidMount()` or `shouldComponentUpdate()`, it is at least possible that you might need to define a parent context in more than one place in the component.  But for most component instances, it's perfectly viable to define that context only once-per-component, right inside the `render()` function.  But you definitely need to define those context references _inside_ the `render()` function.  Otherwise, you won't receive future updates when the parent updates.<br/><br/>

- If this syntax looks a wee bit... "foreign" to you, it might be because I'm imperatively throwing the contexts into a component-scoped `let` variable.  I'm only doing this because you'll need those component-scoped `let` variables if you're referencing those values in other functions tied to the component.  If you prefer to do _all_ of your logic/processing right inside your `render()` function, you can feel free to use the more "traditional" declarative syntax that's outlined in the React documentation.<br/><br/>

- Another reason that I'm highlighting the imperative syntax is because, IMHO, the "default" syntax outlined in the React docs gets a bit convoluted when you want to use _multiple contexts_ inside a single component.  If a given component requires only a single parent context, the declarative syntax can be quite "clean".<br/><br/>

- This solution is **not** ideal if you insist on creating **_One Global Shared State To Rule Them All (And In The Darkness, Bind Them)_**.  You _could_ simply wrap the whole damn app in a single context, and then store **_ALL THE THINGS!!!_** in that context - but that's probably a poor choice.  Redux (and other third-party state-management tools) are better-optimized for rapid updates (e.g., when you're typing a bunch of text into a `<TextField>` and you're expecting the values to be portrayed onscreen with each keystroke).  In those scenarios, the Context API works _just fine_ - assuming that you haven't dumped every damn state variable into a single, unified, global context that wraps the entire app.  Because if you took that approach, you would end up re-rendering _the entire app on every keystroke_.<br/><br/>

- The Context API _excels_ as long as you are keeping `state` where it "belongs".  In other words, if you have a `<TextField>` that requires a simple `state` value to keep track of its current value, then keep the `state` for that `<TextField>` in its parent component.  In other words, keep the `<TextField>`'s state _where it belongs_.  I've currently implemented this in a React codebase with 30k+ LoC - and it works **beautifully** and **performantly**.  The only way that you can "muck it up" is if you insist on using **one** global context that wraps the entire app.<br/><br/>

- As outlined above, the Context API provides a wonderfully _targeted_ way to manage shared state that is part of React's core implementation.  If you have a component that doesn't need to share values with other components, then that's great!  Just don't wrap that component's `render()` function in a context provider.  If you have a component that doesn't need to access shared values from further up the hierarchy, then that's great!  Just don't import the contexts from its ancestors.  This allows you to use as much state management (or as little) as you deem necessary for the given app/component/function.  In other words, I firmly believe that the deliberate nature of this approach isn't a "bug" - it's a _feature_.