Recoil — Another React State Management Library?

Sveta Slepner
The Startup
Published in
9 min readJun 2, 2020

--

There are many React state management libraries, and new ones pop up from time to time. But it is not every day that Facebook themselves introduce a state management solution. Is it any good? Does it bring anything new to the table? Let’s dive in and see if it’s worth your time (spoiler: yes, it does).

Recoil.js — A state management library by Facebook

It was quite something, watching Dave McCabe, A Facebook software engineer, introduce a new state management library during the online React Europe 2020 event on Youtube.
Sure, as of May 2020, Recoil is still experimental (though assumably used in production in some of Facebook’s inner tools), and not quite official, but it’s still interesting to see what led McCabe and his co-workers at Facebook to write this library, which is now open-sourced.

While working on one of their tools with a complex UI and trying to find the best solution for global state management, they hit a performance and efficiency wall. They decided that the best way would be to write their own library.

What’s wrong with Redux or Mobx?

There is nothing wrong with the existing libraries, to be frank. But the thing is, they are not React libraries. The store is something that is handled “externally”, hence doesn’t have access to React’s inner scheduler. Why is it important? For now, it isn’t. But concurrent mode is around the corner, and we can assume that Facebook software developers are using it already. For them, it was important to write a solution that will feel and act React and will be able to easily support concurrency (Recoil is using react state under the hood, and concurrency support should be added in the following weeks).

Also, some libraries (Redux..), while providing robust tools, come with a high cost — to set up even the most basic store, you need to write a lot of boilerplate and verbose code. Furthermore, important features such as dealing with async data, or caching computed selector values, are not part of the base library and require even more third-party libraries solutions. And if God forbid, a selector needs to receive a dynamic prop, memoizing this one correctly is a pain.

What about Context API?

Context API, the native React state sharing solution, also has its limitations.

When used for recurring or complex updates, it’s not that efficient. Quoting Sebastian Markbage, another Facebook engineer:

“My personal summary is that new context is ready to be used for low frequency unlikely updates (like locale/theme). It’s also good to use it in the same way as old context was used. I.e. for static values and then propagate updates through subscriptions. It’s not ready to be used as a replacement for all Flux-like state propagation.

Even the React-Redux team had to revert parts of the library re-written with context API in version 6, simply due to a significant performance hit compared to its previous version (Now React-Redux only uses context to pass down the store reference).

Let’s picture the following scenario. Say, we are rendering a master-detail view with a list of image components and a metadata info component. On an image click, its metadata should be displayed in the info component. We also want the ability to rename the image.

Upon rename, the best outcome would be if we could re-render just the selected image component and the metadata component.

Context API would not make it easy for us to achieve.

For starters, context API doesn’t let you subscribe to a subset of the data it contains.

All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes. (https://reactjs.org/docs/context.html#before-you-use-context)

If our provider’s value is an array or an object, changing any bit of this structure will cause everything subscribed to that context to re-render, even if your component uses just a part of that value. See this demo by Javier Calzado. This means we can’t store all images in a single context, since renaming one will cause everything to re-render (and, sure we can memoize, but it’s not a magic solution and comes with its own limitations).

So let’s say each image will have its own context. There is nothing wrong with that, as long as we know the exact number of images. But what if it is dynamic, and we can add more? We will have to add a Context Provider for each new image, re-shaping the components tree, causing the entire sub-tree to re-mount, which is even worse. See the GIF below for a visual representation of the issue:

Adding a provider dynamically causes the entire sub-tree to re-mount

See how pushing this Context Provider in slide 3 causes everything underneeth to remount?

In addition to not being performant, it introduces tight coupling between the provider and the leaves of the tree.

What does Recoil do differently?

First of all, Recoil is very easy to learn. Its API is very simple and feels natural to people who are already accustomed to using hooks. Getting started is a matter of wrapping your app with RecoilRoot , declaring your data with a unit called atom and replacing useState with Recoil’s useRecoilState.

Secondly, it allows you to subscribe to the exact piece of data your component consumes, declare computed selectors, and it even provides a built-in solution for async data flow.

Creating atoms on the fly with dynamic keys, sending arguments to the selectors, and so on, is also easy peasy (lemon squeezy).

As for support for React concurrent mode, as said earlier, it’s a matter of weeks.

Let’s begin with Recoil’s basics

Atom — An atom is simply put, a piece of state. Think of it like a regular react state, but one that any component can subscribe to. Changing the value of an atom will result in a re-render from all components subscribed to it.

To create an atom we need to provide a key, which should be unique across the application and a default value. The default value can be a static value, a function or even an async function (but on that later).

export const nameState = atom({
key: 'nameState',
default
: 'Jane Doe'
});

useRecoilState — A hook that lets you subscribe to an atom’s value, and update it. Used the same way as you would with useState

useRecoilValue — Returns just the value of the atom, without the setter function.

useSetRecoilState — Returns just the setter function.

import {nameState} from './someplace'// useRecoilState
const NameInput = () => {
const [name, setName] = useRecoilState(nameState);
const onChange = (event) => {
setName(event.target.value);
};
return <>
<input type="text" value={name} onChange={onChange} />
<div>Name: {name}</div>
</>;
}
// useRecoilValue
const SomeOtherComponentWithName = () => {
const name = useRecoilValue(nameState);
return <div>{name}</div>;
}
// useSetRecoilState
const SomeOtherComponentThatSetsName = () => {
const setName = useSetRecoilState(nameState);
return <button onClick={() => setName('Jon Doe')}>Set Name</button>;
}

selector — A selector represents a piece of derived state. It lets us build dynamic data that depends on other atoms. A selector in Recoil is a little different than what you would expect from the word “selector”. It has a mandatory “get” function which acts the same way as reselect with redux or @computed with MobX. But it can optionally accept a “set” function that can update one or more other atoms. We’ll tackle this part later, so for now let’s have a look only on the “selector” part.

// Animals list state
const animalsState = atom({
key: 'animalsState',
default: [{
name: 'Rexy',
type: 'Dog'
}, {
name: 'Oscar',
type: 'Cat'
}],
});
// Animals filter state
const animalFilterState = atom({
key: 'animalFilterState',
default: 'dog',
});
// Derived filtered animals list
const filteredAnimalsState = selector({
key: 'animalListState',
get: ({get}) => {
const filter = get(animalFilterState);
const animals = get(animalsState);

return animals.filter(animal => animal.type === filter);
}
});
// Component that consumes the filtered animals list
const Animals = () => {
const animals = useRecoilValue(filteredAnimalsState);
return animals.map(animal => (<div>{ animal.name }, { animal.type }</div>));
}

Here’s a working demo:

Pretty simple, isn’t it?

Now let’s complicate stuff!

Ok, so now let’s get back to the images app I was talking about earlier, and implement it with Recoil:

The requirements for that app are:
1. We should have the ability to add images dynamically,
2. When renaming, only the selected image and the metadata components should re-render,
3. Images and their data are loaded asynchronously.

To achieve the first two requirements, we are going to store each image in its own atom. For that purpose, we can use atomFamily

atomFamily is the same as a regular atom, only it can receive a parameter differentiates one instance from another. These two are basically the same:

// atom
const itemWithId = memoize(id => atom({
key: `item-${id}`,
default: ...
}))
//atomFamily
const itemWithId = atomFamily({
key: 'item',
default: ...
});

The only differences are that atomFamily will do the memoization for you, and you don’t have to create a unique key for each instance, as again, it’s being done for you.

atom and atomFamily can also call another function to create their default. And same here, the only difference here is that atomFamily can pass over its received id.

export const imageState = atomFamily({
key: "imageState",
default: id => getImage(id)
});

The first time any component will call imageState with that id, it will initiate the function call to create a default.

This function can also be asynchronous, and Recoil will handle it for us, with some help from React’s Suspense

The store’s code:

const getImage = async id => {
return new Promise(resolve => {
const url = `http://someplace.com/${id}.png`;
let image = new Image();
image.onload = () =>
resolve({
id,
name: `Image ${id}`,
url,
metadata: {
width: `${image.width}px`,
height: `${image.height}px`
}
});
image.src = url;
});
};
export const imageState = atomFamily({
key: "imageState",
default: async id => getImage(id)
});

The components:

// Images list
const Images = () => {
const imageList = useRecoilValue(imageListState);
return (
<div className="images">
{imageList.map(id => (
<Suspense key={id} fallback="Loading...">
<Image id={id} />
</Suspense>
))}
</div>
);
};
// Single image
const Image = ({ id }) => {
const { name, url } = useRecoilValue(imageState(id));
return (
<div className="image">
<div className="name">{name}</div>
<img src={url} alt={name} />
</div>
);
};

Demo with the full code:

Before we’re done, Selectors can SET data? WHAT?

When I talked about selectors I mentioned the fact that we pass a set function to a selector. This is weird, but just because the name is conffusing (and hopefuly, will change). Think of selectors like a state, but a derived one. It can get computed values from atoms, and affect multiple atoms as well.

In this example, the selector returns a derived state: A counter object of boxes with a certain color.
It’s setter function can affect all boxes from the box atomFamily and reset them:

const colorCounterState = selector({
key: "colorCounterState",
get: ({ get }) => {
let counter = { [COLORS.RED]: 0, [COLORS.BLUE]: 0, [COLORS.WHITE]: 0 };
for (let i = 0; i < BOX_NUM; i++) {
const box = get(boxState(i));
counter[box] = counter[box] + 1;
}
return counter;
},
set: ({ set }) => {
for (let i = 0; i < BOX_NUM; i++) {
set(boxState(i), COLORS.WHITE);
}
}
});

A full working demo:

Obviously Recoil has many other neat features stored inside, but this at least should get you started.

To summarize, is Recoil worth your time?

We can always ask ourselves “Do we really need another state management library”? And my answer would be: YES!

Why? Because having a state management library that acts and feels like React is refreshing. It means the learning curve is minimal if you ever worked with hooks before. No need to learn new syntax or set tons of boilerplate code to get you going (and I’m aware the selectors get/set syntax is a bit weird, but it really is simple).

It also takes a lot of pain away when it provides solutions to handle async data, state persistency, and parameterized selectors from day one.

I obviously can’t tell you yet how this library scales on large projects, or if it even gets attention and not die, but I sure hope it will get more popular. We can only benefit from that.

Extra reading

--

--

Sveta Slepner
The Startup

Front-end tech lead@Cloudinary and an avid gamer