Skip to content

Recoil

Created: 2020-06-04 16:17:26 -0700 Modified: 2023-05-01 18:52:07 -0700

In my limited experience, Recoil was… fine. I switched to Jotai pretty early on (which took ~30 minutes since I had no knowledge of Jotai) and felt like it was better.

Recoil cons:

  • Still not v1 after 3 years.
  • It seems like the basic cases are harder than in Jotai. In Jotai, selectors and atoms are essentially the same interface. Jotai also doesn’t need an explicit Provider (AKA RecoilRoot).
  • The documentation is quite lacking for the more advanced features like atomFamily or selectorFamily.
  • The documentation was buggy
  • I could not figure out the proper TypeScript typing for selectorFamily.
  • This intro video (May 2020, 33m long) is great to watch
    • Recoil lets you connect disparate parts of the tree via Context without needing a provider for each individual piece of state. These pieces of state are called “atoms”, and they’re both changeable and subscribe-able.
    • You then remove “useState(‘#ffffff’)” with something like “useRecoilState(backgroundColor)”, where “backgroundColor” is an atom. Atoms need unique keys and a default value.
      • useRecoilState has the same semantics as useState, but the state itself is shared.
      • If you need dynamic atoms (e.g. a new one for each new component), you would just make a function that takes an ID and returns an atom:
memoize(id => atom({
key: `item${id}`,
default: {…},
}))

(note: memoize could come from a library like Lodash)

  • Recoil also lets you handle derived state so that you don’t need to manually keep one state in-sync with another. They do this via selectors, which are pure functions plus the information that they depend on.

  • The interface for getting a selector is exactly the same as the one for getting an atom.

  • Any atom or selector can be asynchronous (or even conditionally asynchronous) just by returning a promise.

  • At the end, they mention why you’d want to use this if you’re new to React, and the answer is that you should stick with React until you run into an issue where you want a relationship between two completely separate nodes in the DOM. Dave McCabe says that Recoil is like a “multi-context”.

  • Their getting-started guide is also quick/great (reference). It’s a good idea to work through their TODO app (it doesn’t take too long).

    • An atom is a piece of state
    • Components that read the value of an atom are implicitly subscribed to that atom and will be re-rendered when it’s updated
    • useRecoilState is just like React’s useState:
      • const [someValue, setSomeValue] = useRecoilState(foo);
    • useRecoilValue is the same idea but without a setter:
      • const someValue = useRecoilState(foo);
    • useSetRecoilState is the same idea but without a getter:
      • const setSomeValue = useSetRecoilState(foo); // the component with this line of code will not be re-rendered when the atom is changed since it isn’t reading the value!
    • Selectors represent derived state, which is passing state through a pure function (same inputs == same outputs, and no side effects). Real-world examples: filtering lists, statistics (e.g. number of items in your cart).
      • You can also mutate through a selector with the “set” function. As an example of why you might want to do this, imagine storing a temperature in Fahrenheit and having a selector to convert it to Celsius. You may want to set the temperature through the selector, in which case you would have to convert it to Fahrenheit in the selector itself.

An atom is a piece of state. An atomFamily is a collection of atoms that you can access via parameters that you pass in. For a contrived example, imagine that you have a page representing a directory of users. Without an atomFamily, you would put all of your users into a single atom. However, by doing so, each <User/> component would be updated every time any of them changes.

An atomFamily lets you store each user in their own atom. Furthermore, if using an atomFamily and you want a selector, then you actually want a selectorFamily.

Here’s some code I threw together since I think the documentation is actually terrible for both kinds of families (as of 4/22/2023 at least):

const usersStateFamily = atomFamily<Object, number>({
key: "users",
default: {},
});
const userIdsState = atom<number[]>({
key: "userIdsState",
default: [],
});
const usersSelector = selectorFamily({
key: "usersAccess",
get:
(id: number) =>
({ get }) => {
return get(usersStateFamily(id));
},
set:
(id: number) =>
({ set }, user: Object) => {
set(usersStateFamily(id), user);
},
});
function User({ id }) {
const [user, setUser] = useRecoilState(usersSelector(id));
const updateUser = useCallback(() =>
setUser({...user, counter: counter + 1});
}, [user, setUser]);
useInterval(() => {
updateUser();
}, 350);
const { name, icon, whatever } = user;
return <div>User: {name}</div>;
}
function newUser(id: number) {
return {
id,
icon: "face.png",
counter: 0,
}
}
function AllUsers() {
const userIds = useRecoilValue(userIdsState);
const createUser = useRecoilCallback(
({ set }) =>
(id: number) => {
set(userIdsState, (existing) => [...existing, id]);
set(usersStateFamily(id), newUser(id));
},
[userIdsState, usersStateFamily]
);
return (
<>
<button onClick={() => createUser(Math.ceil(Math.random() * 999999999))}>
Add user
</button>
{userIds.map((id) => {
return <User key={id} id={id} />;
})}
</>
);
}