React State Management with Recoil
Kinga Hunyadi
Posted on January 7, 2021
Recoil is a state management library for React. It is still in experimental phase, but it looks really promising. The best thing about Recoil is that it works and thinks like React. The most important concepts of Recoil are atoms and selectors.
Atoms are units of state, while selectors are pure functions that calculate derived data from state. Selectors accept both atoms and other selectors as input. Components can subscribe to selectors or atoms, and will be re-rendered when the selectors or atoms change.
I will explain how Recoil can manage your application’s state through some examples. No, it won’t be another todo app. Our app will show a list of songs, and for each song we can get some extra info. I will share the GitHub repository at the end of the article.
First of all, we need to create a new React app:
npx create-react-app recoil-example
cd recoil-example
yarn
yarn start
Check that your app works on localhost:3000, you should see a page like this:
Then we need to add Recoil to our app:
yarn add recoil
We need to wrap our components that use Recoil in RecoilRoot. We can replace the content of App.js with:
// App.js
import React from 'react';
import { RecoilRoot } from 'recoil';
import './App.css';
const App = () => (
<div className={'App'}>
<RecoilRoot>
<h1>Recoil Example</h1>
</RecoilRoot>
</div>
);
export default App;
Our app should still work and show the changes we made:
We will create a real-world-like example, so we will start with our client:
// client.js
const songList = [
{ id: 1, title: 'Bohemian Rhapsody' },
{ id: 2, title: 'Purple Rain' },
{ id: 3, title: 'One' },
{ id: 4, title: 'Eternal Flame' },
];
const songDetails = [
{ id: 1, artist: 'Queen', year: 1975 },
{ id: 2, artist: 'Prince', year: 1984 },
{ id: 3, artist: 'U2', year: 1992 },
{ id: 4, artist: 'The Bangles', year: 1989 },
];
export const getSongs = async () =>
new Promise(resolve => setTimeout(() => resolve(songList), 500));
export const getSongById = async id =>
new Promise(resolve => {
const details = songDetails.find(s => s.id === id);
return setTimeout(() => resolve(details), 500);
});
Now that we have our client functions, we can implement the atoms and selectors that will manage our app’s state. Each atom and selector will have a unique id. We will start with loading the song list. As our client function returns a promise, the selector’s get function will be async:
// selectors.js
import { selector } from 'recoil';
import { getSongs } from './client';
export const songsQuery = selector({
key: 'songs',
get: async () => {
const response = await getSongs();
return response;
},
});
Next step is to create a component which renders the list of songs. We need to connect our component to the selector we just created. Recoil has some useful hooks for this:
- useRecoilState — returns the value of the given state and the setter function for updating the value of given the state;
- useRecoilValue — returns the value of the given state;
- useSetRecoilState — returns the setter function for updating the value of given the state.
We will create the Songs component:
// Songs.js
import React from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { songsQuery } from './selectors';
import { currentSongIDState } from './atoms';
const Songs = () => {
const songs = useRecoilValue(songsQuery);
const setCurrentSongID = useSetRecoilState(currentSongIDState);
return (
<>
<h2>Songs</h2>
{songs.map(song => (
<div key={song.id}>
<p onClick={() => setCurrentSongID(song.id)}>{song.title}</p>
</div>
))}
</>
);
};
export default Songs;
We should note that our selector is async, but React render functions are synchronous. Here comes in React Suspense, which handles pending data. We could also handle pending state with Recoil’s Loadable, or implement a handler from scratch, but we will use Suspense now:
// App.js
import React, { Suspense } from 'react';
import { RecoilRoot } from 'recoil';
import Songs from './Songs';
import './App.css';
const App = () => (
<div className={'App'}>
<RecoilRoot>
<Suspense fallback={<span>Loading...</span>}>
<Songs />
</Suspense>
</RecoilRoot>
</div>
);
export default App;
Now in our browser we should see the list of songs:
That was easy, right?
Now let’s see how can we get the details of a song. When we select a song, we want to see its details, like the artist and the year of release. We need to remember the current song ID. The ID is just a simple value, it will not be computed, so we will create an atom for this, instead of a selector:
// atoms.js
import { atom } from 'recoil';
export const currentSongIDState = atom({
key: 'currentSongID',
default: '',
});
Based on the current song ID we want to get the song details. We need another selector which calls the client function with the current song ID. Selectors can read other atoms and selectors using the get argument of the get function. I know it sounds a little confusing, but the next example will make it more clear:
// selectors.js
import { selector } from 'recoil';
import { currentSongIDState } from './atoms';
import { getSongs, getSongById } from './client';
// ...
export const currentSongQuery = selector({
key: 'currentSong',
get: async ({ get }) => {
const response = await getSongById(get(currentSongIDState));
return response;
},
});
We will now create the CurrentSong component, which renders the details of the selected song:
// CurrentSong.js
import React from 'react';
import { useRecoilValue } from 'recoil';
import { currentSongQuery } from './selectors';
const CurrentSong = () => {
const currentSong = useRecoilValue(currentSongQuery);
return currentSong ? (
<>
<h2>Current Song Details:</h2>
<p>Artist: {currentSong.artist}</p>
<p>Released: {currentSong.year}</p>
</>
) : null;
};
export default CurrentSong;
Then we can add it to our Songs component. The currentSongIDState atom can be updated from the component by using the setter function returned by useRecoilState. (Note that I didn’t want to add it to the App component, because I don’t want to show the “Loading…” state when nothing is selected. Of course, we could structure our app better, but for now it’s just fine):
// Songs.js
import React, { Suspense } from 'react';
import { useRecoilValue, useRecoilState } from 'recoil';
import { songsQuery } from './selectors';
import { currentSongIDState } from './atoms';
import CurrentSong from './CurrentSong';
const Songs = () => {
const songs = useRecoilValue(songsQuery);
const [currentSongID, setCurrentSongID] = useRecoilState(currentSongIDState);
/*
* as an alternative, we could declare them separately:
* const currentSongID = useRecoilValue(currentSongIDState);
* const setCurrentSongID = useSetRecoilState(currentSongIDState);
*/
return (
<>
<h2>Songs</h2>
{songs.map(song => (
<div key={song.id}>
<p onClick={() => setCurrentSongID(song.id)}>
{song.title} {song.id === currentSongID && '*'}
</p>
</div>
))}
{currentSongID && (
<Suspense fallback={<span>Loading...</span>}>
<CurrentSong />
</Suspense>
)}
</>
);
};
export default Songs;
If we click on a song we should see the details below the song list:
It was easy and fun so far, while working with read-only data, but in real-world apps we want our app’s state to get updated after doing an update on the server. For example, we might want to add new songs to our list. Here it becomes a little more complicated.
If you are used to work with other state management libraries, like Redux, then you know that the “global” state can be updated after updating the data on the server. Recoil does not have a “global” state, like other state management libraries, but coupled to RecoilRoot. That means the state can not be updated outside of the components/hooks.
But there is still hope... with Recoil we can achieve this by subscribing to server updates from useEffect, and updating the state from there. I know this is not ideal, but this API is still under development, and Recoil might handle this out-of-the-box.
In conclusion, comparing it with other state management libraries (like Redux), it seems more “React-like” and simpler and easier to learn, so it might be a good alternative in the future.
You can find the GitHub repository here. Thank you for reading this article.
Posted on January 7, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.