Recoil — Another React State Management Library?

Sveta Slepner
Jun 2, 2020 · 9 min read

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?

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?

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?

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

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!

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?

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?

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

The Startup

Get smarter at building your thing. Join The Startup’s +794K followers.

Thanks to Eitan Peer

Sign up for Top 10 Stories

By The Startup

Get smarter at building your thing. Subscribe to receive The Startup's top 10 most read stories — delivered straight into your inbox, once a week. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Sveta Slepner

Written by

Frontent developer @Cloudinary and an avid gamer

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +794K followers.

Sveta Slepner

Written by

Frontent developer @Cloudinary and an avid gamer

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +794K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store