State Management with React Relink
Tags: react, statemanagement, javascript
Published: 2020-11-27 13:00:35

Relink is a React state management library inspired by Recoil.

Recoil is Facebook's [experimental state management library](https://recoiljs.org). Shifting from Redux, I have been using Recoil for more than half a year and so far it worked well me. But the downside is that the documentations are not very complete and it comes with features that I find myself never using. For things that I *do* need, I find myself resorting to rather awkward workarounds.

One thing that I have yet to find a workaround is to get it to work with React Native Navigation. In <abbr title="React Native Navigation">RNN</abbr>, each screen has a separate React component tree. State updates do not occur across screens since every screen is wrapped in their own <RecoilRoot>.

Sure, there are other tools out there that can help with state management, but since it can also be an interesting learning experience for me, I've decided to create my own state management solution.

# Relink

I call it Relink (or React Relink, since the name `relink` [has been taken on NPM](https://www.npmjs.com/package/relink)). Part of how Relink works is similar to Recoil. I've made it that way because I find Recoil's convention rather easy to understand.

The source code is currently available on [GitHub](https://github.com/chin98edwin/react-relink) and the package on [NPM](https://www.npmjs.com/package/react-relink). If you find it helpful or simply intriguing, do consider giving it a star on GitHub 😉.

Below are just some basics, detailed documentations are available in the [readme](https://github.com/chin98edwin/react-relink/blob/main/README.md) file.

## 1. No provider components required 🤯

```js
import { createSource, useRelinkState } from 'react-relink'

const ScoreSource = createSource({
  // key must be unique
  key: 'score',
  // This is the default state
  default: {
    red: 0,
    blue: 0,
  }
})

function App() {
  const [score, setScore] = useRelinkState(ScoreSource)
  return /* ... */
}
```

Create a source then use it with a Relink hook and that's it.

Under the hood, Relink hooks use listeners to trigger component updates. The states become accessible (or *linked*) across different React component trees since there are no providers. This is also the main reason it's called "Relink". 

Of course, I can't say for sure if providers are necessary and whether eliminating the need for providers will cause problems, but that shouldn't be a big concern as long as the keys are unique. 

## 2. Hydration & Persistence 🌱

The code for managing data hydration and persistence are kept close to the source creation. You don't only have a single source of truth, but a single place to keep your hydration/persistence logic.

```js
const counterKey = 'counter'
const counterDefaultState = 1

createSource({
  key: counterKey,
  default: counterDefaultState,
  lifecycle: {
    // Hydration
    init: ({ commit }) => {
      const data = localStorage.getItem(counterKey)
      commit(data ? JSON.parse(data) : counterDefaultState)
    },
    // Persistence
    didSet: ({ state }) => {
      localStorage.setItem(counterKey, JSON.stringify(state))
    },
    // Persistence by cleaning up
    didReset: () => {
      localStorage.removeItem(counterKey)
    },
  }
})
```

## 3. Extra Options ⚙️
**• Suspense components during hydration**
By default, hydration happens synchronously. If you're fetching data from the server, then you'll either need to turn this on or conditionally render a loading UI while hydration is in progress. This is disabled by default because it relies on an experimental React feature.

**• Enable mutability**
In case you desperately need some performance improvement, you can enable mutability. This is disabled by default because it might lead to unwanted side effects.

**• Virtual batching**
Meant to improve performance by batching Relink's listener updates before triggering component updates on top of React's `unstable_batchedUpdates`. This is disabled by default because it used to result in faulty component updates in the early stages and the improvements are not obvious.

```ts
createSource({
  key: string,
  default: any,
  options: {
    suspense: boolean,
    mutable: boolean,
    virtualBatch: boolean,
  }
})
```

<hr/>

*(extras)*

# A Funny Observation

There's a funny thing that I learned along the way. At first I wanted to make it usable in React & React Native using the same bundled code but apparently it lead to bugs 🐛. In the end, I had to create different bundles for React DOM and React Native.

As mentioned previously, Relink uses listeners. At first, I relied on `useEffect` to add/cleanup the listeners and it created a rather confusing error. Imagine 3 components subscribing to a listener. The listener callbacks are called from components A to C in a for-loop.

```
┳━━━ <ComponentA />
┗━┳━ <ComponentB />
  ┗━━━ <ComponentC />
```

```js
const keyStack = Object.keys(listeners)
for (const key of keyStack) { listeners[key]() }
```

The callback for Component B is called and there's a chance it can cause Component C to unmount. Then when calling the callback for Component C, the callback becomes undefined since it has been removed in the cleanup function.

Using `for (... i < listeners.length ... )` or `for (... i < Object.keys(listeners).length ... )` seemed to help a little, but it is still possible for the array of callbacks to change before a loop can complete. 

In the end, I resorted to `useLayoutEffect` and React's `unstable_batchedUpdates`. This helped to batch the renders together and solved the problem. However, the logic for batching component updates for browsers and mobile platforms are different so they need to be imported from either 'react-dom' or 'react-native' depending on the environment. Hence, different code bundles need to be generated.

I have also considered using linked lists but yet to test it out. Since it's already working, I'm leaving the code as it is for now. `:3`

# The Bottom Line

Don't reinvent the wheel, use what's already made by others — this is usually true. But when they start to function awkwardly, you might wanna consider manufacturing your own wheels.

Facebook created Recoil to tailor to their needs despite already having several state managing solutions out there. The same can be said about Relink. 

Of course, nothing is perfect. If state management is important to you but nothing's working quite right and you have the capacity, may be you should try creating a solution that fits you too. 🍻