Reimplementing Recoil’s Core APIs for Fun and Learning

Stefano Magni
JavaScript in Plain English
8 min readApr 21, 2021

--

I’m always eager to explore learning paths (you can read more about it in my Choose what NOT to study and focus on one thing at a time article), but I was still missing re-implementing APIs. An article by Kent C. Dodds inspired me, and here we are.

We use Recoil at work; it’s a core element of WorkWave RouteManager's next architecture. Recoil has good ease of use, and it removes every distinction between local and global state management. It’s not perfect yet but relatively stable.

Please note: This article remained unpublished for eight months. I should have rewritten it because I realized that code and design decisions are more understandable through visuals, leaving the code at the end. But since done is better than perfect, I decided to publish the article as is. As a reminder for myself, I think Maty Perry’s “Layout projection: A method for animating browser layouts at 60fps” is a perfect example of how to write a technical article.

Requirements

The goal was re-implementing the atom and the selector APIs, only the sync version. It means

  • implementing the atom API to create new atoms
  • implementing the selector API to create new selectors that depend on atoms and other selectors
  • implementing the useRecoilValue API to get an atom/selector value and subscribing (aka getting re-rendered) to their update
  • implementing the useRecoilState API to get all useRecoilValue features plus setting the atom/selector
  • implementing the RecoilRoot API to avoid sharing the state between different component trees (I need it just for the sake of the tests)

Before diving into the code, what we need is:

  1. storing the values of the Atoms: the atoms themselves are just plain objects, the Recoil store must keep their current state
  2. updating the subscribed components when an Atom updates and forcing them to re-render
  3. exposing API through React hooks
  4. differentiate every store with an id since every RecoilRoot is independent
  5. keep a RecoilRoot’s ID private, we don’t want to expose internal implementation details

Possible solutions:

  • stored values (#1) will be Plain Objects
  • for #2 we need to register a callback for every subscriber
  • #3 the only way we can force a component to re-render is to keep an internal state and update it. How other state libraries do it? Looking at its internals, Recoil does it through useState:
const [_, setValue] = useState({});
forceUpdate = () => setValue({});

While Redux, internally, does it through useReducer

const [, forceRender] = useReducer(s => s + 1, 0)
  • I take #4 for granted nowadays 😉
  • #5 requires us to use a React Context, owned by the RecoilRoot component
  • We’ll manage #6with some higher-order functions that wrap the core ones (later, I’ll explain how to do that)

What about Selectors?

  1. Selectors are stateless. Their get are pure functions that derive their values from other Atoms and Selectors
  2. Selectors must update when one of the Atoms/Selectors they depend on update
  3. Selectors can also write values of other Atoms and Selectors

Therefore:

  • for #1, we need some sugar around the core functions we need for the Atoms
  • #2 forces us to find out the Atoms and the Selectors a Selector depends on and subscribe it to them
  • #3 leverages the existing Atom setters and nothing much more

The gist of it: at the core of the project, a store must collect values and subscribers; subscribers can be components using the provided hooks or selectors.

You can play with the project on CodeSandbox or fork it on GitHub. Below, I’ll guide you through the relevant code split by context: core/public types, core/public API, and the hooks.

Types

If you want to skip the explanation: go directly to the typings.ts file on GitHub.

We will describe the exposed API/types later. Let’s concentrate on what data we need to store internally first. I’ll distinguish internal functions from the public ones prefixing them with core.

CoreRecoilValue

The internal Recoil value. Every Atom should:

  • have an identifying key
  • have a default value
  • have a current value
  • have a list of subscribers to notify when it updates

While every Selector should:

  • have an identifying key
  • have a list of subscribers to notify when it updates

So the signature of CoreRecoilValue is the following:

The CoreRecoilValue signature. Check it out on Gist.

Please note that:

  • we don’t need to pass T while creating the Atom because TypeScript could infer it from the default value
  • there are multiple ways to design this type, but I think discriminated unions are quite concise and clear

RecoilStores

Every RecoilRoot must have a dedicated store and a unique id. A RecoilStore is a Record that identifies the Recoil values by their key and RecoilStores is a Record that stores every RecoilStore by their Recoil id. It’s easier to see the code 😊

The RecoilStores signature. Check it out on Gist.

Public types

There’s nothing to say about public types since I’m replicating Recoil’s type definitions 😊

The public types brought directly from Recoil. Check it out on Gist.

You can take a look at the whole typings.ts file on GitHub.

Core API

If you want to skip the explanation: go directly to the core.ts file on GitHub.

A mix of internal API and utilities, the end-user will not know about them.

Getters

The most effortless functions are the getters. They should:

  • retrieve the current value of Atoms from the RecoilStore or call the selector.get function
  • expose a generic coreGetRecoilValue that consumes the above-mentioned specialized getters
  • a higher-order function (createPublicGetRecoilValue) that allows leveraging the coreGetRecoilValue without knowing the recoil id
The getters implementation. Check it out on Gist.

Please note:

  • Core functions are marked as @private to enforce the idea that the end-user must not import them.
  • the registerRecoilValue is the first point of contact with the active Recoil store (the one the component resides in) and the Atom itself

If you are not familiar with the higher-order functions pattern, it’s a way to pre-configure a function you need to call later. Moreless all the core functions need to know the Recoil id, but, at the same time, their public counterpart needs to hide the id.

Here is a super-concise (without arrow functions) Gist illustrating the idea. Take a look at the logId and the logIdWithoutKnowingIt functions, they do the same thing, but the latter doesn’t require the id.

A super-simple example of higher-order-functions. Check it out on Gist.

Setters

The basic set functionalities are:

  • setting an Atom new value
  • invoking all the subscribers
  • in the case of Selectors, calling their set function (if defined)
  • exposing the usual Recoil id-free functions

Here’s the code:

The setters implementation. Check it out on Gist.

Registration and Subscription

So far everything is plain Vanilla JS, let’s jump into React’s domain: registration and subscription.

Registration must be idempotent (the effect of calling it once or more times must be the same). Why? Because we must register Recoil Values when we need it (because of the Recoil Id stored in the React Context that we can’t know in advance) and the possible options are:

  • checking if the Recoil Value is already registered before trying to register it
  • calling the registerRecoilValue without caring about previous registration, it does check itself

I opted for the latter, making registerRecoilValue idempotent.

The subscribeToRecoilValueUpdates must only return an unsubscriber, here the code for both registration and subscription:

Registration and subscription implementation. Check it out on Gist.

You can take a look at the whole core.ts file on GitHub.

React API

If you want to skip the explanation: go directly to the api.ts file on GitHub.

useRecoilValue and useRecoilState

Here the basic API. useRecoilValue must:

  • retrieve the current Recoil Id from the React Context created by the RecoilRoot component
  • register the Recoil Value
  • subscribe the component to every Atom/Selector update (like the official counterpart does)
  • get the current Recoil Value’ value (like the official counterpart does)

useRecoilState leverages useRecoilValue to retrieve the current value, then it must

  • provide a setter for Atoms
  • provide a setter for Selectors that means invoking the Selector’ set, if defined, passing it both a Recoil Value getter and a setter

That’s the first use of the higher-order functions we defined earlier. Remember that we need to let the consumer access some core functionalities (like setting an Atom) hiding the Recoil Id, that’s why we created the createPublicGetRecoilValue and the createPublicSetRecoilValue.

Here’s the code

UseRecoilValue and useRecoilState implementation. Check it out on Gist.

Subscription

The last missing bit is how the component subscribes to Recoil Value updates and how it unsubscribes. This necessary behavior comes for free with the React.useEffect hook. The tricky part is getting the dependencies’ tree from a Selector and subscribing to every Recoil Value update: createDependenciesSpy does it.

The subscription implementation. Check it out on Gist.

You can take a look at the whole api.ts file on GitHub.

RecoilRoot

The parent of every Recoil tree. Its sole task is creating the Recoil Context around the children that consumes the Recoil Values. The code is straightforward.

The RecoilRoot implementation. Check it out on Gist.

It works!

Here we are! You can play with the project on CodeSandbox or fork it on GitHub. The App.tsx code stresses what mentioned above:

  • registering some Atoms and Selectors, the same way you would do with Recoil
  • registering Selectors that depends on Atoms and Selectors
  • providing a Selector’ set
  • logging every re-render of the components because you can check with your eyes that everything updates as expected while using the app, but you can’t check how many times the components are re-rendered, so take a look at the console

Tests

I don’t want only to check that everything works as expected for this project’s sake, but I want to be sure that components don’t re-render more than expected. I needed to do that so often during the project’s development that I’ve written some tests to automate my manual checks.

FAQs

Are there some untested scenarios in the above code?

Yep, I manually tested that everything works when

  • a component dynamically changes the Recoil Values it is subscribed to
  • a Selector sets another Selector

Why double quotes and semicolons?

I didn’t care about customizing the default CodeSandbox’ Prettier configuration 😉

Are there other approaches out there?

Sure, take a look at Bennett Hardwick’ “Rewriting Facebook’s “Recoil” React library from scratch in 100 lines” article. He uses a more class-based approach. Please check it out!

Conclusion

I liked re-implementing the Recoil API because

  • it forced me to take a look at the Recoil internals
  • it exposed me to different problems and so think about wider solutions
  • it exposed me to creating a new type of blog post where I try to explain some design decisions, my previous articles were much about problems and solutions, telling my experience, or trying to inspire people

I didn’t like doing that because

  • I know that working on side-projects is not ideal for me, I’m sometimes too much of a perfectionist 😁

More articles from me that you might enjoy:

And don’t forget to take a look at my UI Testing Best Practices book on GitHub.

About me

Hi 👋 I’m Stefano Magni, I’m a passionate Front-end Engineer, a Cypress Ambassador, and an instructor. I work remotely as a Senior Front-end Engineer for WorkWave.

I love creating high-quality products, testing and automating everything, learning and sharing my knowledge, helping people, speaking at conferences, and facing new challenges.

You can find me on Twitter, GitHub, LinkedIn. You can find all my recent contributions/talks etc. on my GitHub summary.

More content at plainenglish.io

--

--

I’m a passionate, positive-minded Senior Front-end Engineer and Team Leader, a speaker, and an instructor. Working remotely since 2018.