React State Management with Recoil
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-examplecd recoil-exampleyarnyarn start
// client.jsconst 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);});
// selectors.jsimport { 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.jsimport 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 (< />Songs
{songs.map(song => (setCurrentSongID(song.id)}{song.title}))});};export default Songs;
// App.jsimport React, { Suspense } from 'react';import { RecoilRoot } from 'recoil';import Songs from './Songs';import './App.css';const App = () => (Loading... );export default App;
// atoms.jsimport { atom } from 'recoil';export const currentSongIDState = atom({key: 'currentSongID',default: ''});
// selectors.jsimport { 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;}});
// CurrentSong.jsimport React from 'react';import { useRecoilValue } from 'recoil';import { currentSongQuery } from './selectors';const CurrentSong = () => {const currentSong = useRecoilValue(currentSongQuery);return currentSong ? (< />Current Song Details:
Artist: {currentSong.artist}
Released: {currentSong.year}
) : null;};export default CurrentSong;
// Songs.jsimport 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 (< />Songs
{songs.map(song => (setCurrentSongID(song.id)}>{song.title} {song.id === currentSongID && '*'}}))}{currentSongID && (Loading...}> })});};export default Songs;
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.