Hacking React Hooks: Shared Global State
Tags: react, hooks, tutorial, javascript
Published: 2020-04-06 17:21:10

I'm going to show a dead-simple, possibly "hacky", technique for sharing global state and stateful logic between functional components with Hooks.

## The Problem

I've lost count of how many times I've heard, or read, that Hooks can be used to "share state between components".  It feels to me like this has become a de facto _mantra_ of the React/Hooks crowd.  And yet, every time I've tried to confirm this mythical capability with working real-life code, the results have been... underwhelming.

It's not that you _can't_ share state with Hooks.  It's just that many of the proposed methods either 

1. Leverage the same old techniques that we could always use in class-based components (with the same drawbacks),

1. Or they veer off into complex-and-abstract solutions that are obtuse and potentially brittle.

In the "same story, different day" category, Hooks have excellent support for the Context API.  And this can certainly be extremely useful.  But the Context API can't share state between two _sibling_ components unless the state is saved higher up the chain.  

And of course, we can "share" state by passing it down through props.  But we've always been able to do that, it's subject to the same hierarchy limitations as the Context API, and most of us hate it.

In the "new solutions" category, I've already seen too many proposed approaches that leverage `useReducer()`, `useCallback()`, `useEffect()`, Higher Order Hooks, and the powdered tailbone of a virgin pterodactyl.  

## The Goal

I want to have a single function/Hook that can keep its own state, share that state with anyone who wants it, and pass render updates to any components that are reading that state.  I want that component to be accessible from anywhere in the app.   And finally, I need for any updates to its state to be controlled through a single interface.

Oh... and I want the implementation to be _ridiculously simple_.

Am I asking too much?  I don't think so.  But it's amazing how many wildly-different approaches you can find to this problem across the interwebs.

## A "Default" Approach With Custom Hooks

We have three siblings - Larry, Curly, and Moe.  We also have Curly's child - Curly Jr.  Between the four of them, each of them needs to display the current value of the counter.  In other words, the value of the counter needs to be a _shared_ value.

Furthermore, Larry, Curly, Moe, and Curly Jr all have different responsibilities for _updating_ the counter.  Whenever an update occurs, the value needs to be reflected with each person.  (A live, working example of following code can be found here: https://stackblitz.com/edit/custom-hooks-shared-state-bfadtp)

[**Disclaimer**:  As you look at these examples, you might be thinking that it would be optimal to achieve these results - in _this_ example - by passing state through props.  Or even, by using the (awesome) Context API.  _And I would agree with you._  But it's hard to illustrate the potential benefits of a global state management solution if I have to drop you right into the middle of my Big Hairy App.  So I'm obviously using an extremely simplified scenario to illustrate how this approach might work _on a far larger app_.  I trust that you can extrapolate from these examples...]

```javascript
// index.js
const App = () => {
  return (
    <>
      <Larry />
      <Curly />
      <Moe />
    </>
  );
};

// use.counter.js
export default function useCounter() {
  const [count, setCount] = useState(0);
  const decrement = () => setCount(count - 1);
  const increment = () => setCount(count + 1);
  const invert = () => setCount(count * -1);
  const reset = () => setCount(0);
  return {
    count,
    decrement,
    increment,
    invert,
    reset
  };
}

// curly.jr.js
export default function CurlyJr() {
  const counter = useCounter();
  return (
    <div style={{ marginBottom: 20, marginLeft: 150 }}>
      <div>Curly Jr: {counter.count}</div>
      <div>
        <button onClick={counter.invert}>Invert</button>
      </div>
    </div>
  );
}

// curly.js
export default function Curly() {
  const counter = useCounter();
  return (
    <div style={{ marginBottom: 20 }}>
      <div style={{ float: "left" }}>
        <div>Curly: {counter.count}</div>
        <div>
          <button onClick={counter.decrement}>Decrement</button>
        </div>
      </div>
      <CurlyJr />
    </div>
  );
}

// larry.js
export default function Larry() {
  const counter = useCounter();
  return (
    <div style={{ marginBottom: 20 }}>
      <div>Larry: {counter.count}</div>
      <div>
        <button onClick={counter.increment}>Increment</button>
      </div>
    </div>
  );
}

// moe.js
export default function Moe() {
  const counter = useCounter();
  return (
    <div style={{ clear: "both" }}>
      <div>Moe: {counter.count}</div>
      <div>
        <button onClick={counter.reset}>Reset</button>
      </div>
    </div>
  );
}
```

We have a custom Hook - `useCounter()`.  `useCounter()` has its own state to track the value of `count`.  It also has its own functions to `decrement()`, `increment()`, `invert()`, and `reset()` the value of `count`.  

Larry, Curly, Moe, and Curly Jr all use the custom Hook `useCounter()`.  They all display the value of `count`.  And they each have their own button that is intended to either `decrement()`, `increment()`, `invert()`, or `reset()` the `count` variable.  

If you load up this example in the StackBlitz link above, you'll see that this code... doesn't work.  Everyone is using _the same custom Hook_.  But they are not getting _the same global value_.  

When you click on Larry's "Increment" button, only _his_ counter increments.  The others are unchanged.  When you click on Curly's "Decrement" button, only _his_ counter decrements.  The others are unchanged.

Why does this happen?  Well, the Hooks docs are pretty clear about how this works:

> **Do two components using the same Hook share state?** No. Custom Hooks are a mechanism to reuse _stateful logic_ (such as setting up a subscription and remembering the current value), but every time you use a custom Hook, all state and effects inside of it are fully isolated.

So a custom Hook is, by default, designed to share stateful _logic_, but it doesn't directly share _state_.  Hmmm...  That's incredibly unhelpful.

The docs go on to further explain that:

> Each _call_ to a Hook gets isolated state.

In other words, even though Larry, Curly, Moe, and Curly Jr are all calling the same Hook - `useCounter()`, each of those calls results in a fresh copy of `count`.  So when, for example, Larry updates `count` with `increment()`, Curly, Moe, and Curly Jr are all oblivious to the fact - because _their isolated versions of `count` have not been updated at all_.

## Global State With A Single Hook Instance

It's not enough for Larry, Curly, Moe, and Curly Jr to all use the same custom Hook.  If they're going to truly share state, then they need to also share _the same call_ to that custom Hook.  It won't work for them all to create their own call to `useCounter()`, because that will spawn four separate instances of `useCounter()`'s state.  

But how do we do that?

(A working example of the following code can be seen here: https://stackblitz.com/edit/custom-hooks-shared-state)

```javascript
// global.js
export default {};

// index.js
const App = () => {
  global.counter = useCounter();
  return (
    <>
      <Larry />
      <Curly />
      <Moe />
    </>
  );
};

// use.counter.js
export default function useCounter() {
  const [count, setCount] = useState(0);
  const decrement = () => setCount(count - 1);
  const increment = () => setCount(count + 1);
  const invert = () => setCount(count * -1);
  const reset = () => setCount(0);
  return {
    count,
    decrement,
    increment,
    invert,
    reset
  };
}

// curly.jr.js
export default function CurlyJr() {
  return (
    <div style={{ marginBottom: 20, marginLeft: 150 }}>
      <div>Curly Jr: {global.counter.count}</div>
      <div>
        <button onClick={global.counter.invert}>Invert</button>
      </div>
    </div>
  );
}

// curly.js
export default function Curly() {
  const decrement = () => {
    global.counter.count = global.counter.count - 1;
  };
  return (
    <div style={{ marginBottom: 20 }}>
      <div style={{ float: "left" }}>
        <div>Curly: {global.counter.count}</div>
        <div>
          <button onClick={decrement}>Decrement</button>
        </div>
      </div>
      <CurlyJr />
    </div>
  );
}

// larry.js
export default function Larry() {
  return (
    <div style={{ marginBottom: 20 }}>
      <div>Larry: {global.counter.count}</div>
      <div>
        <button onClick={global.counter.increment}>Increment</button>
      </div>
    </div>
  );
}

// moe.js
export default function Moe() {
  return (
    <div style={{ clear: "both" }}>
      <div>Moe: {global.counter.count}</div>
      <div>
        <button onClick={global.counter.reset}>Reset</button>
      </div>
    </div>
  );
}
```

In this revised version, Larry, Curly, Moe, and Curly Jr all have access to the truly-global state variable `count`.  When any single person performs an action to update `count`, the change is displayed on all the other people.  

When Larry's "Increment" button is clicked, the change is seen on everyone.  The same goes for Curly Jr's "Invert" button and Moe's "Reset" button.

Also notice that Larry, Curly, Moe, and Curly Jr _are not even importing or directly calling `useCounter()` at all_.  A _single instance_ of `useCounter()` was loaded into a simple JavaScript object (`global`) inside `<App>`.  

Once we have a reference to `useCounter()` sitting in the `global` object, Larry, Curly, Moe, and Curly Jr need only `import` that same `global` object to reference the state values and the functions made available through `useCounter()`.  

However, Curly's "Decrement" button doesn't work.  Why is that??

## Controlled Access to Global State

Well, Curly got lazy and tried to directly update the global variable without going through the `useCounter()` custom Hook (that's saved in the `global` object).  Curly tried to get cute by simply doing:

```javascript
global.counter.count = global.counter.count - 1;
```

But that has no effect.  It doesn't update the value in `global.counter.count`.  

This is a tremendously _good_ thing.  It avoids the nightmare of having a global variable that can be updated directly from dozens of different places in the app.  In this implementation, the `count` variable can _only_ be updated in the `useCounter()` custom Hook.  

This also means that `useCounter()` can control what update methods are exposed to the subscribers.  So if we don't want other components to have the ability to `increment()` the `count` value, that's easy.  We just don't return the `increment()` function inside `useCounter()`.

## The Verdict

To be completely honest, this approach _feels_ really good to me.  It's soooo much cleaner than using third-party NPM packages or global state management libraries.  I really _love_ the Context API (and the awesome support for it in Hooks), but that approach isn't always ideal when you want to truly share data in real time across all branches of an application hierarchy.  And the protected nature of the `useCounter()` Hook means that we can control _if_ or _how_ state updates are made.

You may not be too keen on the idea of using that generic, plain-ol'-JavaScript object `global` as a place to cache the instance of `useCounter()`.  It's _possible_ to also cache that call into an `<App>` variable, that is then shared with its descendants via the Context API.  However, I wouldn't necessarily recommend that approach.  

Because if we're using the Context API at the top level of the application to store/pass the `global` object, then _every update to the global object will trigger a re-render of the entire app_.  That's why, IMHO, it's best to keep that plain-ol'-JavaScript object `global` _outside_ of the "regular" life-cycle of the React components.

Are there critical flaws in this approach?  Maybe.  What am I overlooking???