Getting Started Recoil with React

Amir Mohammad Fakhimi
10 min readSep 29, 2023

--

Hi everyone!

In this article, I want to describe what Recoil does in React, its basic features, and how to use it to integrate with React. At last, I implement a simple project using React, Recoil, and TypeScript. Hope you enjoy it! 😉

It’s one of the three parts of my React state managers collection:

Ok, What does Recoil do in React?

As you know, there is a concept named state in React. In short, Recoil is a state management tool with React. It is a library that you can add to your project easily using npm. Imagine there is a state in the left-down part of the DOM tree in your React project. If you want to use that state in the right-down of the DOM of your project, traditionally you should pass the state and some callbacks to achieve the goal. But using Recoil, you just need an import in the right-down component! It’s the benefit and the magic of Recoil.

Recoil Core Concepts

I got help from Recoil official documentation to write this part.

Atoms

Atoms are units of state. They’re updatable and subscribable: when an atom is updated, each subscribed component is re-rendered with the new value. They can be created at runtime, too. Atoms can be used in place of React local component state. If the same atom is used from multiple components, all those components share their state.

Atoms are created using the atom function:

export const searchResultsState = atom({
key: 'searchResultsState',
default: []
})

Atoms need a unique key, which is used for debugging, persistence, and for certain advanced APIs that let you see a map of all atoms. It is an error for two atoms to have the same key, so make sure they’re globally unique. Like React component state, they also have a default (initial) value.

To read and write an atom from a component, we use a hook called useRecoilState. It's just like React's useState, but now the state can be shared between components:

const [searchResults, setSearchResults] = useRecoilState(searchResultsState)

Selectors

A selector is a pure function that accepts atoms or other selectors as input. When these upstream atoms or selectors are updated, the selector function will be re-evaluated. Components can subscribe to selectors just like atoms, and will then be re-rendered when the selectors change.

Selectors are used to calculate derived data that is based on state. This lets us avoid redundant state because a minimal set of state is stored in atoms, while everything else is efficiently computed as a function of that minimal state. Since selectors keep track of what components need them and what state they depend on, they make this functional approach very efficient.

From the point of view of components, selectors and atoms have the same interface and can therefore be substituted for one another.

Selectors are defined using the selector function:

const getNumberOfResultsState = selector({
key: 'getNumberOfResultsState',
get: ({get}) => {
const searchResults = get(searchResultsState)
return searchResults.length
}
})

The get property is the function that is to be computed. It can access the value of atoms and other selectors using the getargument passed to it. Whenever it accesses another atom or selector, a dependency relationship is created such that updating the other atom or selector will cause this one to be recomputed.

Selectors can be read using useRecoilValue(), which takes an atom or selector as an argument and returns the corresponding value. We don't use the useRecoilState() as the getNumberOfResultsState selector is not writeable. If you are interested in writeable selectors, you can see Recoil official document:

const numberOfResults = useRecoilValue(getNumberOfResultsState)

Ok, Great! 🥳

Now you know the basic and main features of Recoil in a React project.

Now let’s implement a simple search engine to learn Recoil in practice. 🔥

Definition of The Project (Search Engine)

We have some bus Ids ranging from 1 to 10. These buses move from an origin to a destination in a specific day number. We search (filter) between all available buses and return the bus ids which match the search. I won’t pay attention to the UI and my focus is in Recoil. The UI is like this:

There are three fields at the top, Origin, Destination and the Day. The search result will appear at the bottom as a table. It also shows the number of found results.
The UI of the search engine

Project Introduction

First, the project is available here.

Next, there are some fake results. The type of each result is:

type resultType = {
busId: number,
origin: string,
destination: string,
day: number,
}

And the type of all results is:

type resultsType = resultType[]

Example of a fake result:

const results: resultsType = [
{
"busId": 7,
"origin": "Claremont",
"destination": "Calistoga",
"day": 11
},
{
"busId": 8,
"origin": "Compton",
"destination": "Barstow",
"day": 26
}
]

You can find used fake results here.

There is also a fake results generator which is written in Python if you are interested:

import json
import numpy as np

np.random.seed(42)
cities = ['Alameda', 'Alhambra', 'Anaheim', 'Antioch', 'Arcadia', 'Bakersfield', 'Barstow', 'Belmont', 'Berkeley',
'Beverly Hills', 'Brea', 'Buena Park', 'Burbank', 'Calexico', 'Calistoga', 'Carlsbad', 'Carmel', 'Chico',
'Chula Vista', 'Claremont', 'Compton', 'Concord', 'Corona', 'Coronado', 'Costa Mesa', 'Culver City',
'Daly City']
class Bus:
def __init__(self):
self.busId = np.random.randint(1, 11)
self.origin = np.random.choice(cities)
self.destination = np.random.choice(cities)
self.day = np.random.randint(1, 31)

buses = []
for i in range(100):
buses.append(Bus())

with open('data.json', 'w') as f:
json.dump(buses, f, indent=4, default=lambda x: x.__dict__)

In the continuation of the introduction, our project has two main components, The first one is SearchInput which is containing our inputs and search button. The next one is SearchResult which is showing the number of found results and the results, itself.

Preparing a React Project

First, open the directory in which you want to create the project. Then run the below command to create the React project with TypeScript:

npx create-react-app medium-recoil-tutorial --template typescript

Then run cd medium-recoil-tutorial to open the project directory. After, run the below commands to install Recoil in your project:

npm i recoil

At the end open the medium-recoil-tutorial directory with any IDE or editor you want.

If you wish to run the project, easily run the below command:

npm start

Implementing The Project

Result

We put our fake results and the corresponding types in the results.ts file in the medium-recoil-tutorial/src/components/searchResult directory. The defined types are:

export type resultType = {
busId: number,
origin: string,
destination: string,
day: number,
}

export type resultsType = resultType[]

You can download the complete file here.

Index

First, open medium-recoil-tutorial/src/index.tsx. We need to wrap theApp tag in the RecoilRoot tag which is available in the recoil package in order to use Recoil.

To do so, first import the below line:

import {RecoilRoot} from "recoil";

Then this part of the index.tsx has changed:

root.render(
<React.StrictMode>
<RecoilRoot>
<App/>
</RecoilRoot>
</React.StrictMode>
);

App

As I said, There are two main components named SearchInput and SearchResult. So we change the App component as below, easily:

import React from 'react';
// import {SearchInput} from "./components/searchInput/SearchInput";
// import {SearchResult} from "./components/searchResult/SearchResult";

function App() {
return (
<div className="App">
{/*<SearchInput/>*/}
{/*<SearchResult/>*/}
</div>
);
}
export default App;

We comment on those lines because those components do not exist, now. We will uncomment them.

Search Input

First, create medium-recoil-tutorial/src/components/searchInput directory. We created a components folder to have a cleaner project!

Then create SearchInput.tsx file in the above directory. We need three labels, three inputs for each label, and a search button in it. We also need three states (useState()) to store inputs’ value.

import React, {useState} from "react";

export function SearchInput() {
const [originInput, setOriginInput] = useState<string>('')
const [destinationInput, setDestinationInput] = useState<string>('')
const [dayInput, setDayInput] = useState<number>(1)

return (
<div id={'search-input-div'}>
<label htmlFor='origin'>Origin</label>
<input type='text' id='origin' value={originInput} onChange={e => setOriginInput(e.target.value)}/>

<label htmlFor='destination'>Destination</label>
<input type='text' id='destination' value={destinationInput} onChange={e => setDestinationInput(e.target.value)}/>

<label htmlFor='day'>Day</label>
<input type='number' min={1} max={30} step={1} id={'day'} value={dayInput} onChange={e =>
setDayInput(Number(e.target.value))}/>

<button id={'search-button'}>
Search
</button>
</div>
)
}

Now you can add the SearchInput component to your App component (Just uncomment 2 corresponding lines).

Ok, well done. Now we are going to dive into Recoil.

We need a searchResultsState in SearchInput.tsx to set its value when search button clicks. We also need to get its value in SearchResult.tsx component in order to show the results. Value of searchResultsState is of resultsType type.

So, we need a shared state between SearchInput.tsx and SearchResult.tsx components. Now, atom helps us!

To achieve our goal, we define an atom in the SearchInput.tsx file as below:

import {atom} from "recoil";

export const searchResultsState = atom({
key: 'searchResultsState',
default: [] as resultsType
})

We choose a meaningful name (Please pay attention we ended up the name with state keyword.), unique key, and a default (initial) value for it.

Another point is we need to export the searchResultsState because we need to use it in SearchResult.tsx component, too.

Now, let’s set the searchResultsState's value when search button clicks.

To do so, we need a search function to call it when search button clicks. And also, we should set the retrieved results to the state.

To implement the search function, we need just a filter on all results as below:

import {results, resultsType} from "../searchResult/results";

function search(originInput: string, destinationInput: string, dayInput: number): resultsType {
return results.filter(result => result.origin.toLowerCase() === originInput.toLowerCase() &&
result.destination.toLowerCase() === destinationInput.toLowerCase() && result.day === dayInput)
}

We search by ignoring case equality between what the user entered and our all results.

Then the onClick attribute of the search button gets a function that sets the searchResultsState's value. We can get the atom’s setter in two ways:

  • Getting setter with its corresponding value:
import {useRecoilState} from "recoil";

const [searchResults, setSearchResults] = useRecoilState(searchResultsState)
  • Getting just the desired setter. To do that, we can use const useSetRecoilState() which is a Recoil API.
import {useSetRecoilState} from "recoil";

const setSearchResults = useSetRecoilState(searchResultsState)

Here, we just need the setter, so it’s better to use the second approach. Then, the onClick callback function of the search button is as below:

() => setSearchResults(search(originInput, destinationInput, dayInput))

The input of the setSearchResults is our search result.

So the final code of SearchInput.tsx is:

import React, {useState} from "react";
import './SearchInput.css'
import {results, resultsType} from "../searchResult/results";
import {atom, useRecoilState, useSetRecoilState} from "recoil";

export const searchResultsState = atom({
key: 'searchResultsState',
default: [] as resultsType
})

export function SearchInput() {
const [originInput, setOriginInput] = useState<string>('')
const [destinationInput, setDestinationInput] = useState<string>('')
const [dayInput, setDayInput] = useState<number>(1)

const setSearchResults = useSetRecoilState(searchResultsState)

function search(originInput: string, destinationInput: string, dayInput: number): resultsType {
return results.filter(result => result.origin.toLowerCase() === originInput.toLowerCase() &&
result.destination.toLowerCase() === destinationInput.toLowerCase() && result.day === dayInput)
}

return (
<div id={'search-input-div'}>
<label htmlFor='origin'>Origin</label>
<input type='text' id='origin' value={originInput} onChange={e => setOriginInput(e.target.value)}/>

<label htmlFor='destination'>Destination</label>
<input type='text' id='destination' value={destinationInput} onChange={e => setDestinationInput(e.target.value)}/>

<label htmlFor='day'>Day</label>
<input type='number' min={1} max={30} step={1} id={'day'} value={dayInput} onChange={e =>
setDayInput(Number(e.target.value))}/>

<button id={'search-button'} onClick={() => setSearchResults(search(originInput, destinationInput, dayInput))}>
Search
</button>
</div>
)
}

Hooray! This part is finished. Let’s step into Search Result.

Search Result

First, create medium-recoil-tutorial/src/components/searchInput/SearchResult.tsx file. We want to show our results in a form of a table.

Then we need our results , so we’ll have three options here:

  • Getting results using searchResultsState atom and useRecoilValue() API:
import {useRecoilValue} from "recoil";

const searchResults: resultsType = useRecoilValue(searchResultsState)
  • Getting results using searchResultsState atom and useRecoilState() API. Because we just need searchResults without its setter, so this idea is not good:
import {useRecoilState} from "recoil";

const [searchResults, setSearchResults] = useRecoilState(searchResultsState)
  • Creating a selector and using it:
import {selector, useRecoilValue} from "recoil";

const getSearchResultsState = selector({
key: 'getSearchResultsState',
get: ({get}) => {
const searchResults = get(searchResultsState)
return searchResults
}
})

const searchResults = useRecoilValue(getSearchResultsState)

The first and third options are good. Here I choose the third option to using more of selector.

You can see the temporary code of SearchResult.tsx below:

import React from "react";
import {selector, useRecoilState, useRecoilValue} from "recoil";
import {searchResultsState} from "../searchInput/SearchInput";

const getSearchResultsState = selector({
key: 'getSearchResultsState',
get: ({get}) => {
const searchResults = get(searchResultsState)
return searchResults
}
})

export function SearchResult() {
const searchResults = useRecoilValue(getSearchResultsState)

return (
<div id={'search-result-div'}>
<table>
<thead>
<tr>
<th>Bus Id</th>
<th>Origin</th>
<th>Destination</th>
<th>Day</th>
</tr>
</thead>
<tbody>
{searchResults.map((result, index) => {
return (
<tr key={index}>
<td>{result.busId}</td>
<td>{result.origin}</td>
<td>{result.destination}</td>
<td>{result.day}</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}

We get our results in the searchResults variable. Then we show the results in a table to be more beautiful and informative.

Now you can add the SearchResult component to your App component (Just uncomment 2 corresponding lines).

I want to have the number of found results in a variable named numberOfResults. There are two good approaches:

  • Getting results using searchResultsState atom and useRecoilValue() API:
const numberOfResults = useRecoilValue(searchResultsState).length
  • Creating a selector and using it. This approach is used when we have the same complex logic many times in our code:
const getNumberOfResultsState = selector({
key: 'getNumberOfResultsState',
get: ({get}) => {
const searchResults = get(searchResultsState)
return searchResults.length
}
})

const numberOfResults = useRecoilValue(getNumberOfResultsState)

Here same as previous part, I choose the third option to using more of selector.

So, at last SearchResult.tsx is:

import React from "react";
import {selector, useRecoilState, useRecoilValue} from "recoil";
import {searchResultsState} from "../searchInput/SearchInput";

const getSearchResultsState = selector({
key: 'getSearchResultsState',
get: ({get}) => {
const searchResults = get(searchResultsState)
return searchResults
}
})

const getNumberOfResultsState = selector({
key: 'getNumberOfResultsState',
get: ({get}) => {
const searchResults = get(searchResultsState)
return searchResults.length
}
})

export function SearchResult() {
const searchResults = useRecoilValue(getSearchResultsState)

const numberOfResults = useRecoilValue(getNumberOfResultsState)

return (
<div id={'search-result-div'}>
<h2>{numberOfResults} results found</h2>
<table>
<thead>
<tr>
<th>Bus Id</th>
<th>Origin</th>
<th>Destination</th>
<th>Day</th>
</tr>
</thead>
<tbody>
{searchResults.map((result, index) => {
return (
<tr key={index}>
<td>{result.busId}</td>
<td>{result.origin}</td>
<td>{result.destination}</td>
<td>{result.day}</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}

We added numberOfResults as a h2 tag.

CSS

If you want some CSS to make your search engine more beautiful, you can download App.css, SearchInput.css, and SearchResult.css stylesheets, and import them to App.tsx, SearchInput.tsx, and SearchResult.tsx, respectively.

Final Word

Now, You have a beautiful and powerful search engine. 🎉🔥

If you want to get deep into Recoil, you can use Recoil’s official document.

Hope you enjoyed this journey and learned Recoil very well. If you have any problem or suggestion, I’ll get so happy if I hear from you.

--

--

Amir Mohammad Fakhimi
Amir Mohammad Fakhimi

Written by Amir Mohammad Fakhimi

I'm Computer Engineering student, now. I'm also interested in Software Engineering and AI. My website is amfakhimi.com