Getting Started Recoil with React
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:
- Getting Started Redux with React
- Getting Started Recoil with React
- Redux vs. Recoil
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 get
argument 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:
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 anduseRecoilValue()
API:
import {useRecoilValue} from "recoil";
const searchResults: resultsType = useRecoilValue(searchResultsState)
- Getting results using
searchResultsState
atom anduseRecoilState()
API. Because we just needsearchResults
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 anduseRecoilValue()
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.