author: Krasimir Tsonev

Hi there, I'm . Senior front-end engineer with over 13 years of experience. I write, speak and occasionally code stuff. Follow me on Twitter, GitHub, Facebook or LinkedIn

Recreating Facebook's Recoil library

Recreating Facebook's Recoil library Photo by Nick Karvounis

This weekend I decided to play with the new kid on the block - Facebook's Recoil library for managing state. I did the trivial counter example to see how it works. It's pretty simple idea so I wondered how much it takes to replicate its features. I found the exact answer - 70 lines of code. Obviously, my implementation didn't cover everything but it was fun so I decided to share my findings.

(A side note: I did not look at the code of Recoil. I didn't want to be bias on how to write my version.)

What is Recoil?

Let's first define what Recoil does.

  • It's managing our state by keeping it in one place
  • Gives us API for accessing and updating the state from within every component
  • There is an API for implementing selectors

Let's quickly look at how the library works. The first thing is to create some piece of state. In the context of Recoil that's called atom.

import { atom } from "recoil";

const counter = atom({
  key: "counterState",
  default: 0,
});

Later, having the atom counter, we may access its value inside a React component:

function Counter() {
  const [counterValue, setCounter] = useRecoilState(counter);

  return <>{counterValue}</>;
}

There are a couple of interesting points here. First, Recoil allows us to define the state wherever we want. This means, similarly to Redux, that we may isolate the state construction from the rest of the application. Secondly, the library provides the useRecoilState hook which acts as a channel/bridge to the defines atoms. And because that's just a hook we may use it at every level of our React tree. This I think is the core difference between Recoil and the native useState. Recoil targets the application state while useState is for local/component state management.

There is also an idea of selectors (state accessors). A function that combines values from different atoms and returns a single value.

import { selector, useRecoilValue } from "recoil";

const formattedCounterValue = selector({
  key: "formattedCounterValue",
  get: ({ get }) => {
    return `Counter: ${get(counter)}`;
  },
});

// later in a component
const formattedValue = useRecoilValue(formattedCounterValue);

Notice that our selector receives a get helper. We may call it multiple times with different atoms as parameter.

In a nutshell, this is what Recoil offers. Now let's see those 70 locs that I was talking about.

Singleton

As we just learned, Recoil is all about application state. It should be accessible from everywhere and the shell around it needs to be a singleton. That's important because we don't want to have multiple versions of the same data. As a single source of truth there should be only one state instance. To do that we can leverage the well known JavaScript module system. Think about the following mycoil.js file:

const State = {};

export function getState() {
  return State;
}

When we import it the code gets executed once. The next import is not running the code again. The result of the file is cached and gets returned straight away. Which in this case is just the getState function. We may say that the constant State is created in the global for the mycoil.js scope. And because of the module system caching the exported getState has always access to the same object. No mather where that function is used we will always get the same value. This is really convenient for our current needs. That State constant will be the place where we will store the atoms.

Implementing the atom

There is one thing which I don't quite understand in Recoil - why working with atoms requires an instance and the same time a unique key. For me passing around a variable is a unnecessary complication. If we provide a unique key we would expect to identify that atom with that same key and forget about the instance. I decided to follow this type of thinking and build my version around the idea that accessing an atom means knowing its key. So:

export function atom(options) {
  State[options.key] = {
    key: options.key,
    value: options.default,
    get() {
      return this.value;
    },
    set(newValue) {
      this.value = newValue;
  };
}

A function that accepts atom options - key and default value. In the global State object we store an entry with get and set methods. this in this.value refers to the atom's entry, not to the State object.

Subscriptions

One other very important characteristic of the Recoil's atom is the subscription model. Every time when we use useRecoilState we are basically subscribing to changes (it's the same with useState hook by the way). When the value changes the component gets re-rendered. We need to replicate this feature and we are going to use exactly useState hook. But before that let's provide an API for adding and removing subscribers:

export function atom(options) {
  State[options.key] = {
    key: options.key,
    value: options.default,
    subscribers: [],
    get() {
      return this.value;
    },
    set(newValue) {
      this.value = newValue;
      this.subscribers.forEach((s) => s(newValue));
    },
    subscribe(callback) {
      this.subscribers.push(callback);
    },
    unsubscribe(callback) {
      this.subscribers = this.subscribers.filter((s) => s !== callback);
    },
  };
}

When the value is changed (in the set method) we loop over the subscribers and call them with the new value.

Replicating the useRecoilState hook

I decided to call my experiment mycoil so our version of the hook will be useMycoilState. Here is the full source code:

export function useMycoilState(key) {
  if (!State[key]) throw new Error(`There is no state matching key ${key}`);

  const state = State[key];
  const [bridgeValue, setBridgeValue] = useState(state.get());

  useEffect(() => {
    const subscription = (updatedValue) => {
      setBridgeValue(updatedValue);
    };
    state.subscribe(subscription);
    return () => {
      state.unsubscribe(subscription);
    };
  }, []);

  return [
    state.get(),
    (newValue) => {
      state.set(newValue);
    },
  ];
}

To access an atom we will only need to know its key. So, our version of the hook accepts a string and not an atom. The very first thing that we do is to check its validity.

If there is a state behind the provided key we are good to go. We have to subscribe when the component is mounted and unsubscribe when the component is unmounted. That's done via the useEffect hook.

We use the subscribe method of the atom to call a function that updates a locally created state. That's the bridgeValue. It's mirroring the atom's value and its role is to re-render the component when the atom gets updated.

At the end of the function we return the atom's value and a setter function. Here it's important to say that we can't just call setBridgeValue. This will indeed re-render the current component but will not affect the atom itself. We have to pass any new values to the atom and let it notifies its subscribers.

Selectors

The selectors in Recoil consist of two functions - one for defining the selector and another one that uses it. We have selector and useRecoilValue. Which in our case will be selector and useMycoilValue.

The first one is really simple. We just have to store a get function (the selector) somewhere.

const Selectors = {};

export function selector(options) {
  Selectors[options.key] = options.get;
}

Same as the State constant, Selectors constant becomes global singleton.

Then useMycoilValue needs to return the result of the selector and pass a get function for fetching atom values.

export function useMycoilValue(key) {
  if (!Selectors[key])
    throw new Error(`There is no selector matching key ${key}`);

  const selector = Selectors[key];
  return selector({
    get(stateKey) {
      if (!State[stateKey])
        throw new Error(`There is no state matching key ${stateKey}`);

      const state = State[stateKey];
      return state.get();
    },
  });
}

There is something very interesting in how Recoil selectors work. I have made some tests and notice that the selector is not just a value accessor. There is a subscribing mechanism too. So if I use a selector in a component, that component gets re-rendered when the atoms used in the selector are updated. While with the implementation above this is not happening.

Final words

At a first glance Recoil is super simple. However, I can come with a couple of edge cases which are not covered by the code above so the implementation of those features is not so trivial. The library itself is I believe useful and has its place in the React ecosystem. It's kind of doing half of what Redux does. We have no API for complex reducer-like logic. Such logic needs to live directly in the React components. But if you are fine with this Recoil is a good state management tool.

If you like the implementation above I wrapped it into a npm module - mycoil.

If you enjoy this post, share it on Twitter, Facebook or LinkedIn. Or maybe comment below: