Outerspace Operation
Tags: react
Published: 2019-10-12 11:17:31


## Background

Recently I'm working on a project generator. I think a blueprint to create a planet is a good analogy. The blueprint is how we want users to build this certain type of planet. And inside this blueprint, there are some core features which we want to share, some for consistency, some others for future exploration. So we extract them out and put them in a "headquarter". The headquarter then exports a few app layer things such that the planets created will look alike. It also provides some APIs to satisfy feature and customizability needs.

## Problem

I've been bugged by the thoughts around some "very procedural API". What I mean by _very procedural API_ is something that looks like this:

```jsx
import { someOuterSpaceOperation } from 'headquarter'

const MyPlanet = () => (
  <div onClick={() => someOuterSpaceOperation('hi headquarter')}>
    called from my planet
  </div>
)
```

Namely, we don't want the API to look like anything related with the component's life cycle. So no hooks, better no syntax sugars like `connect` neither. I'm free to hide the logic somewhere, maybe by the boundary of a file import, after which the user of the API may spell the magic code `someOuterSpaceOperation()`.

The but the action is not _that_ outer space. In connects back to certain component inside `headquarter`, and `someOuterSpaceOperation` may result in a component update. And in the remainder of the universe, we want creatures on those planets to be able to call `someOuterSpaceOperation()` without realizing we'll still on the nether of a same React app.

## Solution

Honestly, I was very, very stuck with this because I felt my imagination was very limited by the React APIs and the existing libraries. I had some rough thoughts around creating and maintaining some kind of store on our own, maybe write some flexible JavaScript to subscribe to it. But I was unable to see how that should actually happen, or where exactly relevant code should go, until [Jinjiang](https://jiongks.name) sent me an example demo, which I then developed to [this codesandbox](https://codesandbox.io/s/outerspace-operation-1f1m0).

Rough idea: Create a store and have `headquarter` subscribe to it. The setter side of the store, however, we export to the planets to consume directly.

So my tiny store will look like this. It maintains a store value, a setter and a getter function, and a way to subscribe to it. It has a couple of assumptions: the store value must be an object, and it allows only one listener. Both assumptions are satisfied in the use case with our `headquarter`.

```js
class Store extends Object {
  constructor(initValue) {
    super(initValue)
    if (typeof initValue === 'object') {
      this.value = initValue
    } else {
      this.value = {}
    }
    this.listener = null
  }
  get = () => this.value
  merge = newValue => {
    this.value = { ...this.value, ...newValue }
    if (typeof this.listener === 'function') {
      this.listener(this.value)
    }
  }
  subscribe = cb => {
    if (typeof cb === 'function') {
      this.listener = cb
    }
  }
}

export default Store
```

With this `Store` class, we can create a store and export the very procedural API we desired, it is free to be called outside of a component's lifecycle,

```js
import Store from './store'
export const outerspaceHQStore = new Store({ agent: 0 })

// setAgent can be called outside of a component's lifecycle
export const setAgent = agent => outerspaceHQStore.merge({ agent })
```

Now in our headquarter, subscribe to the store and put that store value in a stateful variable, then inside a context.

```jsx
const AgentProvider = ({ children }) => {
  // manage subscription here
  // put in react component tree an internally maintained stateful variable
  // that is subscribed to the newest val
  const [val, setVal] = React.useState(outerspaceHQStore.get())
  outerspaceHQStore.subscribe(newVal => setVal(newVal))

  return <AgentContext.Provider value={val}>{children}</AgentContext.Provider>
}
```

Here I used something I learned from [Jamie's hook-based Unstated library](https://github.com/jamiebuilds/unstated-next), wrapping and re-exporting the context provider allows us to keep all the logic about this API in one place.

Then, the users of our very procedural API is able to call `setAgent` anywhere they want, like this:

```jsx
const ComponentThatSetsAgent = () => (
  <button
    onClick={() => {
      setAgent(Math.ceil(Math.random() * 1000))
    }}
  >
    call outerspace!
  </button>
)
```

Then inside `headquarter`, we're able to grab the variable subscribed to our store from its corresponding context:

```jsx
const ComponentThatDisplaysAgent = () => {
  // grabs the value from context
  const { agent } = React.useContext(AgentContext)
  return <h1>received call from agent {agent}</h1>
}
```

## Other thoughts

First time I do silly things with React, feels a bit hacky, and very unsure. Turns out, I'm home cooking state management with a connector, with a very naive subscription? Also, the contrast is stiff. I mean, I previously thought it was out of my imagination but it seems pretty obvious to me now.

## Relevant links

- [demo codesandbox](https://codesandbox.io/s/outerspace-operation-1f1m0)
- [Jamie's hook-based Unstated library](https://github.com/jamiebuilds/unstated-next)