by author Origho Precious
Among all the state management libraries available for use in React apps, Redux is the most popular even ahead of React’s Context APIs. There are also other awesome state management libraries used in React apps one of which is Recoil. Recoil unlike Redux is very easy to set up, even easier to set up than the new Redux toolkit library.
In this article, we will learn how to use Recoil to manage states in React apps instead of Redux.
What is Recoil?
According to documentation,
Recoil is a state management library for React applications.
Recoil is an open-source state management library with more than 14k stars on Github, it was invented by Dave McCabe, a Software Engineer at Facebook. It provides a global state so all components in a React application can share states easily and it is minimal compared to Redux with no boilerplate code setup needed.
Recoil provides a data-graph that flows from shared states into React components. The two core concepts of Recoil according to the official documentation are:
- Atoms, which are units of the global state provided by Recoil, components can access and subscribe to changes made to them.
- Selectors with which we can transform states either synchronously or asynchronously, and components can also access and subscribe to.
Why Use Recoil?
Considering that we have many other state management libraries out there and also React’s Context API and component state, why then should we use Recoil in our React apps?. Let’s highlight some of the reasons below:
- Recoil just like Redux provides a global state. With Recoil, we won’t have to pass states as props down to children components in order to share them between components (a concept known as prop drilling).
- Once we hook up a component to any Atom or Selector, they are subscribed to it, so any update made to that piece of state will be reflected across the app to wherever it’s being used.
- With Recoil Selectors, we can transform a state synchronously or asynchronously and use the derived state anywhere in our app.
- Recoil is minimal and requires no boilerplate code to get started. Redux is very popular but many developers still frown at it because of the amount of code they need to write to set it up.
Building a React App with Recoil
We have now gone over what Recoil is, its core concepts, and why you should consider using it.
In this section, we will build an anime-quote-generator
, this app will fetch quotes from an external API based on selected animes.
Let’s get started by generating a new React app with the command below.
npx create-react-app anime-quote-generator
After that, open it in your favorite code editor.
Next, we will install Recoil and get started building our app components. Run the command below to install Recoil.
yarn add recoil
OR
npm install recoil
Let’s configure our app to use Recoil. Navigate to src/index.js
, here, we only need to wrap our entire app with RecoilRoot
; a Recoil component. Let’s do that below.
import React from "react";
import ReactDOM from "react-dom";
import { RecoilRoot } from "recoil";
import "./index.css";
import App from "./App";
ReactDOM.render(
<RecoilRoot>
<App />
</RecoilRoot>,
document.getElementById("root")
);
We have successfully set up Recoil in our React app just by wrapping all our app’s components with it. You see how easy the setup is, to do this using Redux we will have to write some lines of code to create a store before wrapping our app with the React-Redux Provider component that will contain the store.
Now that we have set up Recoil in our app, let’s start building components, pages, and sharing states with Recoil.
Building AnimePill Component
This component will render the title of an anime passed to it, and when clicked, it will route us to a page where we’ll see quotes from that anime. First, we need to install react-router-dom for routing between pages in our app and styled-components for styling. Let’s do that with the command below:
yarn add react-router-dom styled-components
OR
npm install react-router-dom styled-components
Next, let’s create a folder in the src
folder, called components
. In the components
folder, create a folder called AnimePills
and a file called AnimePills.jsx
inside that folder. The path to this file from src
should be src/components/AnimePills/AnimePills.jsx
, now add the code below to that file.
import { Link } from "react-router-dom";
import styled from "styled-components";
const AnimePill = ({ anime, color }) => {
return (
<StyledPill style={{ background: color }}>
<Link to={`/anime/${anime}`}>{anime}</Link>
</StyledPill>
);
};
const StyledPill = styled.div`
border-radius: 999px;
& a {
display: block;
text-decoration: none;
color: #333;
padding: 1rem 2rem;
}
`;
export default AnimePill;
Above, we just created a component called AnimePills
. This component takes in 2 props; anime
and color
, with the anime we will construct a link using the Link
component from react-router-dom
and we will use the color as a background-color. We then style the component with styled-components
.
Building Quote and SmallQuote components
We will be building 2 components in this section. Let’s start with the Quote
component. Inside our components
folder, create a new folder called Quote
and a file Quote.jsx
inside it. In this component, we will simply render a quote from Naruto and style the component with styled-components
. Add the code below to the file.
import styled from "styled-components";
const Quote = () => {
const quote = {
anime: "Naruto",
character: "Pain",
quote:
"Because of the existence of love - sacrifice is born. As well as hate. Then one comprehends... one knows PAIN.",
};
return (
<StyledQuote>
<p>"{quote.quote}"</p>
<h4>
<span className="character">{quote.character}</span> <em>in</em>{" "}
<span className="anime">{quote.anime}</span>
</h4>
</StyledQuote>
);
};
const StyledQuote = styled.div`
background: #dbece5;
padding: 3rem 5rem;
display: flex;
flex-direction: column;
align-items: center;
& > p {
font-size: 2rem;
letter-spacing: 2px;
text-align: center;
font-style: italic;
margin-bottom: 3rem;
background: #fff;
border-radius: 0.5rem;
padding: 3rem;
}
& > h4 {
font-size: 1.5rem;
font-weight: 500;
letter-spacing: 2px;
span {
padding: 5px 10px;
}
em {
font-size: 1.2rem;
}
& > .character {
background: #b8dace;
}
& > .anime {
background: #f5e7e4;
}
}
`;
export default Quote;
Next, let’s create the SmallQuote
component. This component expects 3 props (anime, character and quote), we will render these props, and style the component with styled-components
. To do this, create a folder inside src/components
called SmallQuote
, inside it create a file SmallQuote.jsx
and add the code below
import styled from "styled-components";
const SmallQuote = ({ quote, character, anime }) => {
return (
<StyledQuote>
<p>"{quote}"</p>
<h4>
<span className="character">{character}</span> <em>in</em>
<span className="anime">{anime}</span>
</h4>
</StyledQuote>
);
};
const StyledQuote = styled.div`
background: #dbece5;
padding: 1.5rem 2.5rem;
display: flex;
flex-direction: column;
align-items: center;
& > p {
font-size: 1rem;
letter-spacing: 2px;
text-align: center;
font-style: italic;
background: #fff;
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
& > h4 {
font-size: 1rem;
font-weight: 500;
letter-spacing: 2px;
span {
padding: 3px 5px;
}
em {
font-size: 1rem;
}
& > .character {
background: #b8dace;
}
& > .anime {
background: #f5e7e4;
}
}
`;
export default SmallQuote;
Above, we just built a component called SmallQuote
and styled it with styled-components
. It is very similar to Quote
component, the purpose of splitting them is to make the code easier to understand. So if you want to make the Quote
component reusable to include features of the SmallQuote
component, feel free to do that.
Next, we will be building our app atoms and selector.
Building Our App Global State (Atoms and Selector).
To get started, navigate into our src
folder and create a folder called store
, inside this folder create a file called index.js
. In this file, we will build all the atoms we need and also a selector to modify one of the atoms. Let’s start with the first atom, animeTitles
. Add the code below to create our first atom.
import { atom } from "recoil";
export const animeTitles = atom({
key: "animeTitleList",
default: [],
});
Above, we created an Atom by importing atom
from recoil and called it animeTitles
. We then defined the required properties of an atom, which are
-
key
- this should be a unique ID among other atoms and selectors we will create in the app. -
default
- this is the default value of the atom.
Doing this with Redux, we will have to create an action creator with a specific type, the action creator will return the type and a payload passed to it, and also we will create a reducer to update our redux store. but with Recoil we don’t need to handle all of that, with the key
prop, Recoil knows the part of the global state to update so when we pass data to update the state it does it correctly and we won’t need to compare action types as we would do in Redux.
Following the same pattern, let’s create another atom called animePageNum
. we will use this atom to hold page number, this will help us properly handle pagination in this app. Add the code below to this file.
export const animeListPageNum = atom({
key: "animeListPageNum",
default: 0,
});
Next let’s create a selector to mutate the array of data we will have in the animeTitles
atom by slicing the array to return only 50 items at a time based on the page number which we will get from animePageNum
atom.
export const slicedAnimeTitles = selector({
key: "slicedAnimeTitles",
get: ({ get }) => {
const animes = get(animeTitles);
const pageNum = get(animeListPageNum);
const newAnimeList = [...animes];
const arrIndex = pageNum === 0 ? 0 : pageNum * 50 + 1;
return newAnimeList.splice(arrIndex, 50);
},
});
Above, we created a selector called slicedAnimeTitles
, we defined a key
property just like we did in the atoms we created above, and here we have a new property get
whose value is a function, this is only available in selectors, this function has 2 parameters but here we are using just one of them that is get
with which we can access the value of an atom or selector. Inside this function, with the get
method, we saved the animeTitles
and animeListPageNum
atoms into 2 variables animes
and pageNum
respectively, and with the pageNum
we specified the index to start slicing from and then returned a new array of just 50 items.
We have now successfully created all the shared states we will be using in this app. Next. Let’s create a pagination component, to handle user click and update the animeListPageNum state (atom) so we can update the list of animes we are returning from the selector we just created.
Building Pagination Component
To begin, navigate to src/components
and create a new folder Pagination
, inside it create a file Pagination.jsx
, paste the code below into this file.
import { useState, useEffect } from "react";
import { useRecoilState } from "recoil";
import styled from "styled-components";
import { animeListPageNum } from "../../store";
const Pagination = ({ listLength }) => {
const [pageNum, setPageNum] = useRecoilState(animeListPageNum);
const [numsArr, setNumsArr] = useState([]);
Above, we created a new component Pagination
. This component has a prop listLength
; this will help us determine the page numbers to render. We then imported [useRecoilState](https://recoiljs.org/docs/api-reference/core/useRecoilState)
, which accepts a state as an argument just like React’s useState hook, This also works similar to useSelector
in Redux But here we can update the state directly and not have to dispatch an action. We can access the value of the state passed to useRecoilState
and also update the state, We also created a component state with useState
hook to hold an array of numbers, these will be the page numbers to render in this component.
useEffect(() => {
const paginationNums = () => {
const max = Math.floor(listLength / 50);
let nums = [];
for (let i = 0; i <= max; i++) {
nums.push(max - i);
}
setNumsArr(
nums.sort((a, b) => {
return a - b;
})
);
};
paginationNums();
}, [listLength]);
return (
<StyledPagination>
{numsArr?.length
? numsArr?.map((num) => (
<button
className={pageNum === num ? "active" : ""}
onClick={() => setPageNum(num)}
key={num}
>
{num + 1}
</button>
))
: null}
</StyledPagination>
);
};
In the useEffect we imported above, we created an array of numbers with the listLength
prop, and updated the numArr
state with the array we created and then we looped through the array of nums and rendered them in buttons, each button will update the animeListPageNum
when clicked. Let’s complete this component by adding the code below.
const StyledPagination = styled.div`
display: flex;
align-items: center;
border-width: 2px 2px 2px 0;
border-style: solid;
width: max-content;
& button {
outline: none;
background: transparent;
border: none;
border-left: 2px solid;
width: 35px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
&:hover,
&.active {
background: #fae1da;
}
}
`;
export default Pagination;
With the Pagination
component done, we can now build our app pages and complete the app.
Building Homepage Component
In this section, we will build our homepage
component, this page will render a static quote and also a list of animes using the AnimePill
component we created earlier and also the Pagination
component for pagination. To do this, in our src
folder let’s create a folder called pages
, in this folder create a folder called home
and a file inside it called index.jsx
, folder path should src/pages/home/index.jsx
. Add the code below to this file
import { useRecoilValue } from "recoil";
import styled from "styled-components";
import AnimePill from "../../components/AnimePill/AnimePill";
import Pagination from "../../components/Pagination/Pagination";
import Quote from "../../components/Quote/Quote";
import { slicedAnimeTitles, animeTitles } from "../../store";
const Homepage = () => {
const animes = useRecoilValue(animeTitles);
const slicedAnimes = useRecoilValue(slicedAnimeTitles);
const colors = ["#FAE1DA", "#E8C6AD", "#F2E2ED", "#D6EBE4", "#BFDCD0"];
const generateColor = () => {
const randNum = Math.floor(Math.random() * 5);
return colors[randNum];
};
return (
<StyledHomePage>
<header>
<h2>Anime Quote Generator</h2>
</header>
<main>
<Quote />
<div className="animes">
<h3>All Animes</h3>
{animes?.length ? (
<p>Click on any anime to see a quote from it</p>
) : null}
<div className="flex">
{animes?.length ? (
slicedAnimes?.map((anime) => (
<div key={anime} style={{ margin: "0 1.3rem 1.3rem 0" }}>
<AnimePill anime={anime} color={generateColor()} />
</div>
))
) : (
<p className="nodata">No anime found 😞 </p>
)}
</div>
{animes?.length > 50 ? (
<div className="pagination">
<Pagination listLength={animes?.length} />
</div>
) : null}
</div>
</main>
</StyledHomePage>
);
};
const StyledHomePage = styled.div`
max-width: 80%;
margin: 2rem auto;
& header {
margin-bottom: 3rem;
& > h2 {
font-weight: 400;
letter-spacing: 3px;
text-align: center;
}
}
& .animes {
margin-top: 4rem;
& > h3 {
font-weight: 400;
font-size: 1.4rem;
background: #ece4f1;
width: max-content;
padding: 0.3rem 1rem;
}
& > p {
margin: 1.2rem 0;
}
& > .flex {
display: flex;
justify-content: center;
flex-wrap: wrap;
& > .nodata {
margin: 2rem 0 4rem;
font-size: 1.3rem;
}
}
& .pagination {
display: flex;
flex-direction: column;
align-items: center;
margin: 2rem 0 4rem;
}
}
`;
export default Homepage;
Above, we imported:
-
useRecoilValue
from the Recoil library to get the state values, -
styled
fromstyled-components
to style theHomepage
component, -
AnimePill
to render anime title, -
Pagination
to handle pagination, -
Quote
to display a static anime quote, -
SlicedAnimeTitles
is the selector we created earlier. We will be its return value on this page, and -
animeTitles
which is the first atom we created to hold the list of animes.
Next, we created a function component called Homepage
inside this component, we accessed the animeTitles
and the slicedAnimeTitles
state using useRecoilvalue
and also we created an array of colors (we will pass these colors to the AnimePill
component at random). We then created a function generateColor
, this component returns a random color from the colors array. After that, we returned the component body styled with styled-components
, a header, the Quote
component, and a little notice telling a user what to do, then if we have animes
, we will loop through the slicedAnimes
and render each of them with the AnimePill
component by passing the anime to the component and also a color prop from the generateColor
function and if there’s none we render a ‘no data’ state.
Next, we are checking to see if the length of the animes
state is more than 50, if true we render the Pagination
component. and finally, we added a block of styled-component
styles
We’ve now successfully created our Homepage
component, in the next section let’s create a page we will be routed to when we click on any AnimePill
. In that component, we will make an API call to the external API and fetch all quotes from the selected anime and render the quotes.
Building Animepage Component
Let’s get started by navigating to our pages
folder, inside create a folder called anime
and a file inside it called index.jsx
. Add the code below to the file.
import { useState, useEffect } from "react";
import { Link, useParams } from "react-router-dom";
import axios from "axios";
import styled from "styled-components";
import SmallQuote from "../../components/SmallQuote/SmallQuote";
const Animepage = () => {
const param = useParams();
const [quotes, setQuotes] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (param?.name) {
setLoading(true);
const fetchAnimeQuotes = async () => {
try {
const res = await axios.get(
`https://animechan.vercel.app/api/quotes/anime?title=${param?.name}`
);
setQuotes(res?.data);
setLoading(false);
} catch (error) {
console.log(error);
setLoading(false);
}
};
fetchAnimeQuotes();
}
}, [param]);
return (
<StyledAnimePage>
<h2>Quotes from {param?.name}</h2>
<Link to="/">Go back</Link>
<div className="grid">
{loading ? (
<p>Loading...</p>
) : quotes?.length ? (
quotes?.map((quote, index) => (
<div key={quote?.quote + index} className="anime">
<SmallQuote
anime={quote?.anime}
character={quote?.character}
quote={quote?.quote}
/>
</div>
))
) : (
<p className="nodata">No Quote found 😞</p>
)}
</div>
</StyledAnimePage>
);
};
const StyledAnimePage = styled.div`
max-width: 80%;
margin: 2rem auto;
position: relative;
& > a {
position: absolute;
top: 1rem;
text-decoration: none;
}
& > h2 {
font-weight: 400;
letter-spacing: 3px;
text-align: center;
margin-bottom: 2rem;
}
& > .grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: max-content;
& .anime {
margin: 1rem;
height: max-content;
}
& > p {
margin: 2rem 0 4rem;
font-size: 1.3rem;
text-align: center;
}
}
`;
export default Animepage;
Above, we imported all the components and hooks we will be using. We also initialized the [useParams](https://reactrouter.com/web/api/Hooks/useparams.)
hook, to get an anime title from our browser URL as a param based on the route we’ll define for this page. Next, we created 2 component states using useState
hook, one to hold the quotes we will fetch from the API and the other for a loading, And in a useEffect
, we are fetching the quotes based on the anime name gotten from the URL and setting the quotes
state with it. We then returned a block of jsx styled with styled-components
.
Above, you’ll notice that we didn’t use Recoil, instead, we saved the response we got from the API request in a useState
, this is because the states will only be used in this component.
Open Source Session Replay
Debugging a web application in production may be challenging and time-consuming. OpenReplay is an Open-source alternative to FullStory, LogRocket and Hotjar. It allows you to monitor and replay everything your users do and shows how your app behaves for every issue.
It’s like having your browser’s inspector open while looking over your user’s shoulder.
OpenReplay is the only open-source alternative currently available.
Happy debugging, for modern frontend teams - Start monitoring your web app for free.
Creating App Routes and Fetching Animes
To complete this app, Let’s navigate to src/App.js
. Here, we will be doing 2 things:
- Fetch a list of animes from the external API and updating the
animeTitles
atom we created earlier with it. - Define our app routes with
react-router-dom
.
Let’s get started. Go to src/App.js
and replace what we have there with the code below.
import { useEffect } from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import { useSetRecoilState } from "recoil";
import axios from "axios";
import { animeTitles } from "./store";
import Homepage from "./pages/home";
import Animepage from "./pages/anime";
Above, We imported
-
useEffect
- We will be making our API call inside it, so that we can fetch the array of anime once the page is rendered. -
BrowserRouter
,Route
, andSwitch
fromreact-router-dom
- We will create our routes with them. -
[useSetRecoilState](https://recoiljs.org/docs/api-reference/core/useSetRecoilState)
fromrecoil
- with this, we will update theanimeTitles
atom just by passing the atom to it as an argument, and -
axios
- for fetching data from the external API
Next, we will create the App component and fetch the animes inside. To do that add the code below.
const App = () => {
const setTitles = useSetRecoilState(animeTitles);
const fetchAnimes = async () => {
try {
const res = await axios.get(
"https://animechan.vercel.app/api/available/anime"
);
setTitles(res?.data);
} catch (error) {
console.log(error?.response?.data?.error);
}
};
useEffect(() => {
fetchAnimes();
}, []);
Above, we created the App component, and inside we created a variable setTitles
with which we will update our animeTitles
atom (state). Next, we created an async function called fetchAnimes
, inside it, we fetched the animes from the external API using axios and updated our animeTitles
state with it while using try-catch for error handling. After that we called the fetchAnimes
inside the useEffect
we imported so this function runs once the page is rendered.
Let’s finish up the App
component by adding routes.
return (
<BrowserRouter>
<Switch>
<Route exact path="/" component={Homepage} />
<Route exact path="/anime/:name" component={Animepage} />
</Switch>
</BrowserRouter>
);
};
export default App;
We have now completed our app. let’s start our dev server to see how it works.
Run the command below in your terminal
yarn start
OR
npm start
If you followed along correctly, you should see these pages.
Click on any anime and see a page like this. I will click on Naruto.
Conclusion
In this article, we learned what Recoil is, why use it, and how to use it instead of Redux by building an anime-quote-generator app using Recoil for state management. We also compared how to do certain things in Recoil to Redux, and we saw how easy it is to use Recoil. You can learn more about Recoil from the official docs.
Top comments (1)
Thanks for sharing the article. Check this devtools for recoil :
github.com/creotip/recoil-gear