Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
March 7, 2020 02:59 am GMT

A Context API Framework for React State Management

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). This has allowed me to hone the approaches in the original approach - and to 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.

  2. Most professional devs have come to see bolted-on state-management tools as a default must have.

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

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.

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

  • 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

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.

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

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

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

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

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

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

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

data.layer.js

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.

  • The text is translated using the translate() function from the <Utilities> context.

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

home.module.js

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.

  • The translate() functions are picked up from the <Utilities> context.

  • The onClick function will trigger logIn() from the <DataLayer> context.

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

  • If this syntax looks a wee bit... "foreign" to you, it might be because I'm imperatively throwing the contexts into a global let variable. I'm only doing this because you'll need those global 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.

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

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

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

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


Original Link: https://dev.to/bytebodger/a-context-api-framework-for-react-state-management-1m8a

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To