Rethinking Prop Drilling & State Management in React
Tags: react, redux, javascript, tutorial
Published: 2020-03-01 23:56:40

My regular readers (both of them) know that I've been doing a lot of thinking (and rethinking) about state management and application architecture in React.  It feels to me like the "standard" approach to React development is to spin up a fresh new project, and then, by default, to shackle that project to the epic scaffolding that is Redux.  

In the first article in this series, I illustrated a particular technique I'd been using to get around "the Redux headache".  In the second article, I explored (and was pleasantly surprised) by the capabilities of the new-and-improved Context API.  

But I haven't stopped turning this one over in my head.  And as much as I truly _like_ the functionality that's available in the Context API, I'm definitely starting to rethink the whole idea that I need to somehow "get around" React's default approach at all (which is referred to, somewhat derisively, as "prop drilling").  

## The (Alleged) Problem

The first time I started reading through the React docs - before I'd written a single line of JSX - I was a little annoyed with the framework's default answer for sharing state/functions/anything.  In fact, I think a lot of people had the same reaction when they first started learning about the framework.  If they didn't, there wouldn't be so many React state-management tools/packages/articles/arguments out there "in the wild".

From where I sit, it seems that the React community has largely coalesced around an informal, unwritten policy that would read something like this:

> Pass values via props whenever you have a direct parent/child relationship between components.  For nearly anything else, reach for a state-management package, and dump _all_ of the shared values into it.  If you wanna earn the React Compliant-Developer Achievement Badge, use Redux.  All the cool kids are doing it.  You wanna be cool... _don't you_???

I believe that most devs reach for a state-management package because they want to avoid this potential headache:

```javascript
export default class TopLevel extends React.Component {
   constructor(props) {
      this.state = {
         value1 : 'a',
         value2 : 'b',
         value3 : 'c',
         value4 : 'd',
      };
   }

   doThis = () => console.log('doThis()');

   doThat = () => console.log('doThat()');

   doSomethingElse = () => console.log('doSomethingElse()');

   render = () => {
      return (
         <>
            <div>Top Level</div>
            <MiddleLevel
               value1={this.state.value1}
               value2={this.state.value2}
               value3={this.state.value3}
               value4={this.state.value4}
               onThis={this.doThis}
               onThat={this.doThat}
               onSomethingElse={this.doSomethingElse}
            />
         </>
      );
   ;
}
```

This is just a simple little component.  It's primary purpose is to set a handful of state variables, declare a few basic functions, and then to `render()` the `<MiddleLevel>` component.  

If there's any "problem" in this component, it comes from the fact that we're not using any state management tool.  So if we need to pass all these values down to `<MiddleLevel>` (or to descendants further down the chain), we need to pass those values through props.  And as our list of values to be passed downward grows, we start to acquire an unwieldy pile of props that have to be stuffed into `<MiddleLevel>`.  

This can feel even _more_ cumbersome if we have a `<MiddleLevel>` component that's something like this:

```javascript
export default class MiddleLevel extends React.Component {
   constructor(props) {
      this.state = {value5 : 'e'};
   }

   doMiddleLevelStuff = () => console.log('doMiddleLevelStuff');

   render = () => {
      return (
         <>
            <div>Middle Level</div>
            <BottomLevel
               value1={this.props.value1}
               value2={this.props.value2}
               value3={this.props.value3}
               value4={this.props.value4}
               onThis={this.props.doThis}
               onThat={this.props.doThat}
               onSomethingElse={this.props.doSomethingElse}
            />
         </>
      );
   ;
}
```

In this scenario, `<MiddleLevel>` isn't really _doing_ anything with all those props that were passed into it.  Well... that's not _entirely_ true.  `<MiddleLevel>` _is_ doing one important thing:  It's passing all those props onto its child - `<BottomLevel>` - where, presumably, `<BottomLevel>` knows what to do with them.

This is why I often think of prop _drilling_ instead as: prop _muling_.  Because we've essentially turned `<MiddleLevel>` into a pack mule by strapping all those unwieldy props onto its back.

(**Note**: I realize that you can remove a lot unnecessary typing here by using `{...this.props}`.  But even if the verbiage is cleaner, the example shown above is still what's actually happening.  The "muling" is still occuring.  So I wanted to illustrate it manually for the purpose of highlighting the difference between this and my proposed solution.)

Of course, this is a simplistic scenario.  There are plenty of examples in _real_ code where a value near the top of a component hierarchy might need to be shared with components that are _dozens_ of levels below it in the hierarchy.  And no one wants to type out all those damn props on all of the intermediary components.  That's why most React devs find themselves reaching for state-management solutions _by default_.  

I will raise my hand and freely admit that, the first couple times I snooped around React as a potential tool for my own use, I saw this is a near-fatal flaw.  And when I finally "gave in" and started doing real React development, I considered a state-management tool to be a _necessity_ on any-and-all React projects.  Because without a formal store in which I could chunk _all_ of my shared values, I'd be faced with the prospect of doing massive prop drilling - and that was never something I was willing to consider.

## Removing The Stigma From Prop Drilling

I've recently begun to reassess my aversion to prop drilling.  It's not that I really _want_ to write code that resembles the example shown above.  That would feel to me like torture.  But prop drilling does have a few major points in its favor:

1. **Prop drilling is _core React_.**<br/>It's the way that the React docs first teach you to pass values between components.  React devs (and JS devs, in general) love to pass around the word "pure".  Pure functions.  Pure components.  Pure malarkey.  (Just kidding... sorta.)  Well, in React, you can't get much more "pure" than passing values through props.

1. **Props are innately _stable_.**<br/>They won't be removed from React any more than arguments will be removed from JavaScript functions.  They'll always be there.  They'll always _work_ as they do today.  You'll never have to worry about whether your prop-handling code will be compliant with future versions of React.  But if you're using a state-management tool, that tool may evolve and change over time.  And you may be forced to refactor legacy state-management code that was previously working just fine as-is.

1. **Prop drilling has _no_ package footprint.**<br/>Obviously, if you're not using _any_ state-management library, then there's no additional concern about package bloat.

1. **Adherence to props helps to write cleaner, _purer_ functions/components.**<br/>I'm still amazed by the number of React devs I encounter who don't understand (or simply won't acknowledge) that `globalStateManagement === massiveDependencyInjection`.  They'll preach about the dangers of entangling dependencies... and then they slap Redux (or some other state-management tool) into nearly all of their components.  But props have no such downside.  Just as arguments are the API to a function, props are the API to a component.  And APIs don't create dependencies.  They insulate applications from dependencies, because they represent a standardized contract between that component and the rest of the app.

1. **Props enforce a uniform, traceable path of information through the app.**<br/>Prop data can only flow in _one_ direction: from parent-to-child.  Yes, a child can invoke a method on the parent (through use of callbacks), but the _data_ can only flow from ancestors down to descendants.  But state stores that exist _outside_ the traditional React structure provide devs a way to circumvent this flow.  This can lead to some really nasty bugs.

## Methods & Memory

But even if you agree with all of the points outlined above, it doesn't solve the laborious headache that awaits us if we choose to pass every value, through every component, in its own unique prop.  At some point, you'll just give up and reach for your favorite, global, state-management tool.  So if we strive to truly pass our values the "original" way - through props - the challenge becomes whether we can find ways to make that process manageable.  Because any process that's inherently unmanageable will eventually be abandoned (or undercut in a very clunky way).

On my latest project, I'm using a technique that I've dubbed *Methods & Memory*.  The idea's pretty simple.  There are two types of values that we need to pass down through the hierarchy - methods (functions) and memory (state).  If we can pass them down to the children without having to explicitly define every damn value, we can make the process much faster - and much cleaner.

The first objective is to combine "methods & memory" into their own composite objects.  With single objects, we can pass the values without having to define loads of individual props.  Of course, with regard to memory (state), those values are _already_ packaged into one, convenient object for each component.

**Memory**

So with that in mind, we're gonna start to build a chain of uniformly-named props that will come to hold the _shared state_ of the entire app.  In other words, we're gonna use one, repeated prop to replace the common store that's provided by other state-management tools.  In our `<TopLevel>` component, that will look like this:

```javascript
export default class TopLevel extends React.Component {
   constructor(props) {
      this.state = {
         value1 : 'a',
         value2 : 'b',
         value3 : 'c',
         value4 : 'd',
      };
   }

   doThis = () => console.log('doThis()');

   doThat = () => console.log('doThat()');

   doSomethingElse = () => console.log('doSomethingElse()');

   render = () => {
      return (
         <>
            <div>Top Level</div>
            <MiddleLevel memory={this.state}
               onThis={this.doThis}
               onThat={this.doThat}
               onSomethingElse={this.doSomethingElse}
            />
         </>
      );
   ;
}
```

There's nothing too radical here.  There are other tools/techniques that call, from time-to-time, for you to pass a component's entire state down to a child.  And that's what we're doing here.  We're using the preexisting `state` object to pass all of this component's state through a single prop.  

By doing this, we were able eliminate the distinct props that were used for `value1`, `value2`, `value3`, and `value4` - because those values are already encapsulated in the `state` object.  This makes the cognitive load of passing `value1`, `value2`, `value3`, and `value4` much smaller because we don't have to spell them all out separately when a child component is rendered.

With our `memory` prop now created, we can continue to pass it down through multiple layers in the hierarchy... with one very-tiny exception.  Here's what the `memory` prop will look like in the `<MiddleLevel>` component:

```javascript
export default class MiddleLevel extends React.Component {
   constructor(props) {
      this.state = {value5 : 'e'};
   }

   doMiddleLevelStuff = () => console.log('doMiddleLevelStuff');

   render = () => {
      const {value1} = this.props.memory;
      if (value1 === 'a')
         console.log('This console.log() will be executed.');
      return (
         <>
            <div>Middle Level</div>
            <BottomLevel memory={{...this.state, ...this.props.memory}}
               onThis={this.props.doThis}
               onThat={this.props.doThat}
               onSomethingElse={this.props.doSomethingElse}
            />
         </>
      );
   ;
}
```

First, I created a conditional `console.log()` to illustrate how we reference `<TopLevel>`'s state values from the `props` object.

Second, just as `<TopLevel>` passed its state to `<MiddleLevel>` via a single `memory` prop, so too will `<MiddleLevel>` pass _the combined, global state_ to `<BottomLevel>` via a single `memory` prop.  By consistently using the same prop name (`memory`) to pass shared state, it makes the code consistent whenever we need to access those values.

As you can see, the value of `memory` is slightly different than it was in `<TopLevel>`.  Since `<TopLevel>` is the entry point for this demonstrated chain of components, there's nothing for `<TopLevel>` to pass except its own state.  And that can be done quite simply with `memory={this.state}`.  

But `<MiddleLevel>` wants to pass _all_ the state.  This means it must pass whichever values were supplied in `this.props.memory` _in addition to_ any values that exist in its own `state`.  So to accomplish this, we use spread operators to create a new value for `memory` that consists of the combined objects.

In theory, we can continue this chain for as long as we like.  Every component receives the shared state values in `this.props.memory` and it passes them to its children with `memory={{...this.state, ...this.props.memory}}`.  By following this pattern, all descendant components will have access to whatever state values were set on their ancestors.

**Methods**

_Memory_ (state) was only one half of the equation in _Methods & Memory_.  There are also times when you need to pass a function (e.g., a _method_), down to a child.  You _could_ stick function references right into the `state` object.  That might feel a little "strange" for a lotta devs.  So I've taken a different approach.  Just as I pass memory (state) through the chain, I'll also pass methods (functions) in their own composite object.  Here's what it looks like in `<TopLevel>`:

```javascript
export default class TopLevel extends React.Component {
   constructor(props) {
      this.state = {
         value1 : 'a',
         value2 : 'b',
         value3 : 'c',
         value4 : 'd',
      };
      this.methods = {
         doThis : this.doThis,
         doThat : this.doThat,
         doSomethingElse : this.doSomethingElse,
      };
   }

   doThis = () => console.log('doThis()');

   doThat = () => console.log('doThat()');

   doSomethingElse = () => console.log('doSomethingElse()');

   render = () => {
      return (
         <>
            <div>Top Level</div>
            <MiddleLevel memory={this.state} methods={this.methods}/>
         </>
      );
   ;
}
```

By passing all the state as a single object, we eliminated the need for individual props, to represent each of the individual state values, when rendering the child.  And with this latest change, we've chunked all the function references into a single object.  Then we pass that object in a single prop called `methods`.  So we no longer need to pass every function in its own unique prop.  

The net effect of these changes is that this:

```javascript
<MiddleLevel 
   memory={{...this.state, ...this.props.memory}}
   methods={{...this.methods, ...this.props.methods}}
/>
```

Is _far_ cleaner, shorter, and more-standardized than this:

```javascript
<MiddleLevel
   value1={this.props.value1}
   value2={this.props.value2}
   value3={this.props.value3}
   value4={this.props.value4}
   onThis={this.props.doThis}
   onThat={this.props.doThat}
   onSomethingElse={this.props.doSomethingElse}
/>
```

So let's look at how we change `<MiddleLevel>` to support this:

```javascript
export default class MiddleLevel extends React.Component {
   constructor(props) {
      this.state = {value5 : 'e'};
      this.methods = {doMiddleLevelStuff : this.doMiddleLevelStuff};
   }

   doMiddleLevelStuff = () => console.log('doMiddleLevelStuff');

   render = () => {
      const {value1} = this.props.memory;
      const {doThat} = this.props.methods;
      if (value1 === 'a')
         console.log('This console.log() will be executed.');
      doThat(); // prints 'doThat()' in the console
      return (
         <>
            <div>Middle Level</div>
            <BottomLevel 
               memory={{...this.state, ...this.props.memory}}
               methods={{...this.methods, ...this.props.methods}}
            />
         </>
      );
   ;
}
```

Just as we did with `memory`, the `methods` prop passed into `<BottomLevel>` is a concatenation of `this.props.methods` (which contains any functions that have been passed down the chain) with `this.methods` (which contains the function references for the current component).  

## A Targeted Approach

This technique ultimately keeps a lot of control in the programmer's hands.  As I've demonstrated it here, we're passing _all_ of the state values from one component down to the next.  But there's no rule that requires you to do as such.  

You may have some `state` variables that you don't _want_ to be shared around to all of the lower-level components in the hierarchy.  And that's fine.  Just don't include them in the `memory` prop.  For the sake of brevity, I've illustrated the technique as using all of the `this.state` object.  But you can always create your own abbreviated object, culled from the component's `this.state` object, that contains only the values you explicitly desire to be shared to the descendants. 

Likewise, you probably don't want _all_ (or maybe even, _most_) of your functions being shared down the hierarchy.  The easy answer to such a concern is: Just don't add those function references to `this.methods`.  You may not like the idea that you have to manually add those references into `this.methods`, but I honestly see that as a _feature_ of this technique - not a _flaw_.  This allows you to think deliberately about what does-or-does-not go into the shared state.

You probably have some components that don't have any need to access their ancestors' state or functions.  That's fine, too.  Just don't pass the `memory` and/or `methods` props to those components.  

I know that some disagree with me on this.  But targeted control is a must for any approach that I choose to use.  I've seen Redux implementations where they've basically crammed **_ALL THE THINGS!!!_** into the global store.  Redux doesn't _make_ you do that.  But some devs take that approach, nonetheless.  I personally prefer any solution that encourages me to think, very carefully, about what I choose to put in shared stated and what remains "private".

## Naming Collisions

The only obvious issue that I've found with this approach so far, is the danger of naming collisions.  For example, you could have five different components in your hierarchy that all have a state variable named `fieldValue`.  But if you're passing the state as I've shown above - by spreading the _ancestors'_ state into the same object along with _this component's_ state - there is the potential for naming conflicts.  So keep that in mind if you choose to play around with this approach.

## Avoiding Knee-Jerk Architectural Decisions

There's nothing _wrong_ with the idea of using a state-management tool.  They certainly have valuable, legitimate uses.  There's nothing _wrong_ with having a hammer in your toolbelt.  Who doesn't appreciate a good hammer??  IMHO, a "problem" only occus when you insist on using that hammer on _every_ project for _every_ task.  

I used to think that shared state management was the hammer that should be used for _every_ task.  I don't much care if you choose to use the technique I've outlined above, or whether you use the Context API, or whether you gaze lovingly upon your (mountain of) Redux code.  The only thing that I care about (for any project that _I'm_ directly involved in), is that we don't make dogmatic decisions based on some lazy assumption - like the assumption that passing shared values through props is somehow unmanageable.

Another aspect of this solution that I enjoy is that it's not an all-or-nothing atomic approach.  You don't need to make any Big Hairy Global Project Decisions where your team argues for three months, and then decides to either throw out their state-management tool and switch to this solution, or _only_ use their state-management tool and forsake this solution.  

This is just the core functionality in React.  It could live quite happily side-by-side with your existing state-management library. You could leverage the technique wherever/whenever you wish and you don't have to try to convince anyone to chuck their Redux/MobX/whatever in the garbage bin. 

As I write these posts and do my own mental processing, not only have I started to realize that shared state management tools are not a universal must-have.  But I'm even coming around to the idea that they should probably be _the exception_, rather than a default tool that's tacked onto _every_ new React project.