Back

Using Recoil instead of Redux For State Management In React Applications.

Using Recoil instead of Redux For State Management In React Applications.

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:

  1. Atoms, which are units of the global state provided by Recoil, components can access and subscribe to changes made to them.
  2. 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 from styled-components to style the Homepage 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

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay 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:

  1. Fetch a list of animes from the external API and updating the animeTitles atom we created earlier with it.
  2. 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, and Switch from react-router-dom - We will create our routes with them.
  • [useSetRecoilState](https://recoiljs.org/docs/api-reference/core/useSetRecoilState) from recoil - with this, we will update the animeTitles 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.

localhost:3000 - homepage

Click on any anime and see a page like this. I will click on Naruto.

anime page showing quotes from 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.

Resources