RecoilJS is meant to rock your React world
RecoilJS
is a state management library for React
which was made publicly available recently by Facebook . The truth is that they have been using it for some time internally, so they finally decided to open-source it ๐
At the moment of this writing RecoilJS
is considered an experimental set of utilities and its API keeps on improving rapidly. Personally speaking I have already tried to take advantage of its ways in a rather big application of mine, with complex state management and many dynamic connections. I have to admit that I got amazed with the efficiency, the simplicity and the flexibility it offers.
In my humble opinion RecoilJS
makes React
hell stronger. Let's see why.
# Why RecoilJS then?
No matter how much we love React
we have to admit that there are some tricky parts or even pain points when it comes about state management with the Hooks API that is offered out of the box. To make it even more clear, we are talking mostly about React.useContext
here and not about third party libraries like Redux
.
One of the issues we face when it comes about state management, is that the children components should constantly inform the ancestors. This might seem quite simple for small applications but for more complex ones, things start getting ugly especially when we are talking about long components trees which we force to get re-rendered with every change triggered by a nested child.
Another issue is that context can store mostly single values and not complex variations needed by different consumers.
All these force the 2 sides of the components tree to be tightly coupled. As declared in RecoilJS
docs:
Both of these make it difficult to code-split the top of the tree (where the state has to live) from the leaves of the tree (where the state is used).
So this is what RecoilJS is trying to accomplish. It tries to make our life easier by providing a Reactish API for even more flexible state management across complex applications.
In order to accomplish this, it defines a graph attached to our React components tree so that state changes flow from atoms
which are the roots of this graph to our components through selectors
which are pure functions.
Here is a small representation that might help ypu visualize what is actually going on with atoms
:
# Show me an atom
Sure!! This is a dead-simple atom
named itemsState
:
import { atom } from 'recoil';
const itemsState = atom({
key: 'itemsState',
default: [{
description: 'Don\'t be lazy, write the post of the week ๐ฌ',
done: false,
}],
});
As we said atoms
are units of state. Each atom
has a unique identifier, and a default value. For our examples we can use a simple todo list.
RecoilJS
offers some hooks we can use to get access to atoms
state . We can use a hook named useRecoilState
to access the list and apply changes to it or just the hook useRecoilValue
to pull the list only.
Let's see a real life example then:
import React from 'react';
import { atom, useRecoilValue } from 'recoil';
const itemsState = atom({
key: 'itemsState',
default: [{
description: 'Don\'t be lazy, write the post of the week ๐ฌ',
done: false,
}],
});
const List = () => {
const items = useRecoilValue(itemsState);
return (
{items.map((item, i) => (
<div key={i}>
{item.description}
</div>
))}
)
}
export default List;
Pretty straightforward right?
# Show me a selector
This is a dead-simple selector
named unfinishedItemsState
that pulls the unfinished items from itemsState
atom:
import React from 'react';
import { atom, selector } from 'recoil';
const itemsState = atom({
key: 'itemsState',
default: [{
description: 'Don\'t be lazy, write the post of the week ๐ฌ',
done: false,
}],
});
const unfinishedItemsState = selector({
key: 'unfinishedItemsState',
get: ({ get }) => {
const items = get(itemsState);
return items.filter(item => item.done === false);
}
});
As we mentioned above, selectors are pure functions, and we use them to pull data from atoms
or even other selectors
. They can get both of these as input, and they get re-evaluated every time state changes. The components that subscribe to them, re-render each time accordingly.
Let's create a more complex selector
by using unfinishedItemsState
selector:
import React from 'react';
import { atom, selector } from 'recoil';
const itemsState = atom({
key: 'itemsState',
default: [{
description: 'Don\'t be lazy, write the post of the week ๐ฌ',
done: false,
}],
});
const unfinishedItemsState = selector({
key: 'unfinishedItemsState',
get: ({ get }) => {
const items = get(itemsState);
return items.filter(item => item.done === false);
}
});
const unfinishedItemsCountState = selector({
key: 'unfinishedItemsCountState',
get: ({ get }) => {
const items = get(unfinishedItemsState);
return items.length;
}
});
We added a new one named unfinishedItemsCountState
that is pulling unfinished items from unfinishedItemsState
selector and returns their length. Easy right?
Let's use unfinishedItemsCountState
in our component then:
import React from 'react';
import { atom, selector, useRecoilValue } from 'recoil';
const itemsState = atom({
key: 'itemsState',
default: [{
description: 'Don\'t be lazy, write the post of the week ๐ฌ',
done: false,
}],
});
const unfinishedItemsState = selector({
key: 'unfinishedItemsState',
get: ({ get }) => {
const items = get(itemsState);
return items.filter(item => item.done === false);
}
});
const unfinishedItemsCountState = selector({
key: 'unfinishedItemsCountState',
get: ({ get }) => {
const items = get(unfinishedItemsState);
return items.length;
}
});
const List = () => {
const unfinishedItemsCount = useRecoilValue(unfinishedItemsCountState);
const items = useRecoilValue(itemsState);
return (
<>
You have {unfinishedItemsCount} unfinished tasks!!
{items.map((item, i) => (
<div key={i}>
{item.description}
</div>
))}
</>
);
}
export default List;
Awesome!!
# Ok, what about native React hooks then?
Nothing special here. We keep on using React hooks same as we did before in our components alongside RecoilJS
hooks .
As we saw above, useRecoilValue
hook returns the items list itself. How can we update this list then? We could use useRecoilState
hook that exposes a method we can use to update the list accordingly.
Time to create a controlled input with the help of React.useState
and then take advantage of useRecoilState
hook to add new items to our list:
import React from 'react';
import { atom, selector, useRecoilValue, useRecoilState } from 'recoil';
const itemsState = atom({
key: 'itemsState',
default: [{
description: 'Don\'t be lazy, write the post of the week ๐ฌ',
done: false,
}],
});
const unfinishedItemsState = selector({
key: 'unfinishedItemsState',
get: ({ get }) => {
const items = get(itemsState);
return items.filter(item => item.done === false);
}
});
const unfinishedItemsCountState = selector({
key: 'unfinishedItemsCountState',
get: ({ get }) => {
const items = get(unfinishedItemsState);
return items.length;
}
});
const List = () => {
const unfinishedItemsCount = useRecoilValue(unfinishedItemsCountState);
const [items, setItems] = useRecoilState(itemsState);
const [value, setValue] = React.useState('');
const handleSubmit = e => {
e.preventDefault();
setItems(items.concat({
description: value,
done: false,
}));
setValue('');
};
return (
<>
You have {unfinishedItemsCount} unfinished tasks!!
{items.map((item, i) => (
<div key={i}>
{item.description}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
<button disabled={!value}>
Add
</button>
</form>
</>
);
}
export default List;
So we created a controlled input by using React.useState
and every time the form gets submitted we are using setItems
method - provided by useRecoilState
- to update the list in itemsState
atom.
The nice thing here is that the counter with the unfinished tasks gets updated automatically whenever we add a new todo item and this is done actually with the minimum effort. Pretty amazing, right?
# Ok, so how all these fit together?
RecoilJS
uses context to make these outstanding bindings, that is why we need to wrap our top-level App
component with RecoilRoot
. This is a context provider provided by RecoilJS
and must be an ancestor of all components that use atoms
and selectors
:
import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot } from 'recoil';
import App from './App';
ReactDOM.render(
<RecoilRoot>
<App />
</RecoilRoot>,
document.getElementById('root'),
);
For sure we can have multiple roots and each of them has its own atoms with distinct values.
When we have nested roots the inner ones mask entirely the outer ones.
# Things we need to pay attention to
Obviously RecoilJS
makes React
state management way easier with the API it provides but there are more we haven't touched yet and we should mention here:
- Selectors provide a very powerful API with a getter and a setter. we might find ourselves using a setter rarely but this actually gives even more flexibility to return writeable state from our selectors:
import { atom, selector } from 'recoil';
const tempFahrenheit = atom({
key: 'tempFahrenheit',
default: 32,
});
const tempCelcius = selector({
key: 'tempCelcius',
get: ({ get }) => ((get(tempFahrenheit) - 32) * 5) / 9,
set: ({ set }, newValue) => set(tempFahrenheit, (newValue * 9) / 5 + 32),
});
- Selectors can use Promises and pull data from an external resource in a dynamic fashion. Because of this we can run pretty complex logic with the minimum boilerplate:
import { atom, selector, useRecoilValue } from 'recoil';
const currentUserNameQuery = selector({
key: 'CurrentUserName',
get: async ({get}) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
});
return response.name;
},
});
function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}
- We can use
React.Suspense
to take care of asynchronous selectors:
function MyApp() {
return (
<RecoilRoot>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</RecoilRoot>
);
}
- We can wrap our components or parts of the application with an
ErrorBoundary
since selectors can throw errors when something goes south:
const currentUserNameQuery = selector({
key: 'CurrentUserName',
get: async ({get}) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
});
if (response.error) {
throw response.error;
}
return response.name;
},
});
function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}
function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
);
}
RecoilJS
treats navigation and state persistence as first class concepts which is absolutely magnificent
So, that was it. I am sure you have lots of questions so I highly propose to play with it in an actual codebase and see how it goes. Feel free to use this codesandbox I put together with the examples above. Cheers!!
You can read the official documentation if you need to go deeper with RecoilJS
Did you like this one?
I hope you really did...
Newsletter
Get notified about latest posts and updates once a week!!
You liked it?