
This post will keep updating along with any updates on Recoil or Redux, as well as your useful opinions. Please stay tuned.
In the recent ReactEurope@2020, Dave McCabe, a Facebook Software Engineer, has introduced Recoil, the new state management tool, that claims to be better featured compared to existing tools such as Redux and MobX. As in May 2020, Recoil is still in experimental phase and many features such as concurrent mode support, server side rendering and etc are still under rapid development.
However, is Recoil really a better state management tool that can help us build a better and performant React app? That is what we are going to find out today. We will compare Recoil against Redux, the most popular state management tool. We will look into how Recoil does all the stuff that Redux can do, as well as if it’s doing less or more than Redux.
TL;DR
Recoil is definitely an exciting tool for state management in React but it is still in an experimental phase, there are a lot things still need to be done before it’s ready for production. Until then, I will suggest staying with Redux and be patient.
Pros
- Recoil can achieve what Redux can with lesser boilerplate.
- Recoil claims to able support concurrent mode but still under experimental.
Cons
- Recoil has yet fully supported state observing like Redux middleware, the observer hook is still under experimental.
- Lack of debugging tools.
- Not officially supported server side rendering.
Recoil Core Concept
Before we jump into the comparison, let’s take a look at the core concepts of Recoil: Atom and Selector.
Atom
You can think of Atom as a unit of state with a key and a default value. It represented each individual state such as shopping cart items, filter selections etc.
import { atom } from 'recoil';
const ShoppingCartItemsState = atom({
key: 'SHOPPING_CART_ITEMS',
default: [],
});
You can also set the default value as Promise if you want to load the default value from the server or etc. In that case, you will need to wrap the components that used the state with Suspense
import { atom } from 'recoil';
const ShoppingCartItemsState = atom({
key: 'SHOPPING_CART_ITEMS',
default: new Promise(),
});
You may read more about atom here.
Selector
Selector, on the other hand, is representing a derived state. It’s very similar to reduxjs/reselect, which allows us to derive or compute the state based on other states.
import { selector } from 'recoil';
const ShoppingCartItemsState = atom({ ... });
const FilteredShoppingCartItemsState = selector({
key: 'FILTERED_SHOPPING_CART_ITEMS',
get: ({ get }) => {
// Get the state from the cart items state
const items = get(ShoppingCartItemsState);
// Filter the cart items
return items.find(item => item.available);
},
});
However, a selector can be writable as well if you provide a set method in the definition. This is useful when you want to update the upstream state. This can be relatively new if you come from Redux background.
import { selector } from 'recoil';
const ShoppingCartItemsState = atom({ ... });
const FilteredShoppingCartItemsState = selector({
key: 'FILTERED_SHOPPING_CART_ITEMS',
get: ({ get }) => {
// Get the state from the cart items state
const items = get(ShoppingCartItemsState);
// Filter the cart items
return items.find(item => item.available);
},
set: ({ set, get, reset }, newValue) => {
// Get upstream state
const items = get(ShoppingCartItemsState);
// Do something with new value
// Update to upstream state
set(ShoppingCartItemsState, newItems);
}
});
You may read more about selector here.
Hooks
Recoil provides several hooks for us to get or set the state, these are few Recoil hooks that are commonly used:
useRecoilState
– Use this hook when you want to get and set the state.
useRecoilValue
– Use this hook when you only want to get the state.
useSetRecoilState
– Use this hook when you only want to set the state.
useResetRecoilState
– Use this hook when you want to reset the state to default.
Perfect, you should have some ideas on what it’s included in Recoil now. Enough with the reading and let’s get into practical exercise.
This tutorial is building a simple Star Wars characters preview using SWAPI. You may clone the CodeSandbox below to follow along.
Don’t worry if you see a blank page now.
Get and set state
First, let’s start by creating an atom for the character list, this state will be used to store the list of characters that we get from servers.
// CharacterState.ts
import { atom } from "recoil";
const CHARACTERS = "CHARACTERS";
export const CharactersState = atom<Character[]>({
key: CHARACTERS,
default: []
});
We use CHARACTERS
as the unique key to identify the state and set the default value as an empty array. Next, we can use the atom in the component to get the state as well as setting the state once we get the response from the server.
// CharacterList.tsx
import React, { useEffect } from "react";
import { useRecoilState } from "recoil";
const CharacterList = () => {
// Use useRecoilState because we want to get and set value
const [characters, setCharacters] = useRecoilState(CharactersState);
useEffect(() => {
const onLoad = async () => {
const people = await loadCharacters();
setCharacters(people);
};
onLoad();
}, [setCharacters]);
return ( ... )
}
Last but not least, we need to wrap the root component with the RecoilRoot
component.
// index.tsx
import { RecoilRoot } from "recoil";
render(
<RecoilRoot>
<App />
</RecoilRoot>,
rootElement
);
Up to this point, you should be able to see a list of Star Wars’ characters loaded on the page.
With a single atom, we have created a shared state of character list across the application. If you use CharacterState
in another component, it will reuse the same state. On the other hand, to achieve the similar functionality with Redux, we will need to create an action creator, reducer, selector. Thus, this is a great Yay for Recoil in reducing the boilerplate.
To show another example, let’s add the functionality which is displaying the character’s name on the right hand side when we click on the character list.
// CharacterState.ts
const SELECTED_CHARACTER = "SELECTED_CHARACTER";
export const SelectedCharacterState = atom<Character | null>({
key: SELECTED_CHARACTER,
default: null
});
// CharacterItem.tsx
import { useSetRecoilState } from "recoil";
import { Character, SelectedCharacterState } from "./CharacterState";
const CharacterItem = ({ character }: { character: Character }) => {
// Use useSetRecoilState because we only want to set state
const setSelectedCharacter = useSetRecoilState(SelectedCharacterState);
const handleClick = () => {
setSelectedCharacter(character);
};
return (
<div
...
onClick={handleClick}
>
...
</div>
);
};
// CharacterDetail.tsx
import { useRecoilValue } from "recoil";
import { SelectedCharacterState } from "./CharacterState";
const CharacterDetail = () => {
// Use useRecoilValue because we only want to get the value
const selectedCharacter = useRecoilValue(SelectedCharacterState);
...
return (
<div className="p-3">
<p>Name: {selectedCharacter.name}</p>
...
</div>
);
};
To explain further, we created a new atom with the key SELECTED_CHARACTER
and set the default value to null. When we click on an item, it will store the whole character data into the state. Then, we can read the character data in the detail component using the same atom.
Asynchronous Update
We have seen the example of synchronous get and set state. How about asynchronous getter and setter? Recoil does support asynchronous getter and setter if we return a Promise instead of a function in the selector.
Let’s say we want to display the origin planet of the selected character and we will need to call an API to get the planet information. Since this is a new piece of information added on top of the SELECTED_CHARACTER
state, instead of creating a new atom, we can create a selector that will depend on the SELECTED_CHARACTER
state.
// CharacterState.ts
import { atom, selector } from "recoil";
import { getPlanet } from "./Character";
const CHARACTER_PLANET = "CHARACTER_PLANET";
...
export const CharacterPlanetState = selector<Planet | null>({
key: CHARACTER_PLANET,
get: async ({ get }) => {
// Get selected character
const selectedCharacter = get(SelectedCharacterState);
if (!selectedCharacter) {
return null;
}
// Get homeland of selected character
const homeland = await getPlanet(selectedCharacter.homeworld);
return homeland;
}
});
There are two ways to handle loading the asynchronous get state:
- Use
useRecoilValue
and wrap the component withSuspense
. - Use
useRecoilValueLoadable
and check for thestate === ‘loading’
You can choose which approach is appropriate and suitable for your use case. Pick either of the code snippets below.
// Use useRecoilValue by wrapping with Suspense component.
// CharacterDetail.tsx
import { SelectedCharacterState, CharacterPlanetState } from "./CharacterState";
const CharacterDetail = () => {
const planet = useRecoilValue(CharacterPlanetState);
...
return (
<div className="p-3">
...
<p>Planet: {planet?.name}</p>
...
</div>
);
};
// App.tsx
import React, { Suspense } from "react";
export default function App() {
return (
<div className="grid grid-cols-4">
...
<div className="col-span-3">
<Suspense fallback="Loading...">
<CharacterDetail />
</Suspense>
</div>
</div>
);
}
// Use useRecoilValueLoadable and check for the state === ‘loading’
// CharacterDetail.tsx
import { useRecoilValue, useRecoilValueLoadable } from "recoil";
import { SelectedCharacterState, CharacterPlanetState } from "./CharacterState";
const CharacterDetail = () => {
const planetLoadable = useRecoilValueLoadable(CharacterPlanetState);
...
if (planetLoadable.state === "loading") {
return <p className="p-3">Loading...</p>;
}
return (
<div className="p-3">
...
<p>Planet: {planetLoadable.contents?.name}</p>
...
</div>
);
};
useRecoilValueLoadable
returns an object that contains two keys:
state
– representing the status of the asynchronous selector. Possible valuesloading, hasError or hasValue
contents
– Value of the selector. If thestate
isloading
, it is the Promise object; If thestate
ishasError
, it is theError
object; If thestate
ishasValue
, it is the resolved value.
We are almost done with the tutorial except the last part, which is how to use useResetRecoilValue
. This hook basically allows us to reset the atom to its default value.
We will implement the functionality to reset the SELECTED_CHARACTER
state when we click on the “Close” button.
// CharacterDetail.tsx
import { ... , useResetRecoilState } from "recoil";
const CharacterDetail = () => {
...
// Use useResetRecoilState to reset state
const resetSelectedCharacter = useResetRecoilState(SelectedCharacterState);
...
return (
<div className="p-3">
...
<button className="border rounded py-1 px-3 mt-3" onClick={resetSelectedCharacter}>Close</button>
</div>
);
};
That is all the basic tutorial and common usage of Recoil in state management. I think it is really straightforward and Recoil does reduce a lot of boilerplate. However, let’s continue to evaluate Recoil in different aspects such as state observing (Redux middleware-like), debugging tools etc.
Middleware
Recoil is still currently developing the useRecoilTransactionObserver.
Testing
Recoil used the pure function and can be tested just like other hooks using Jest or other Javascript testing library.
Debugging
Since Recoil has just announced recently, it does not have any debugging tools like Redux devtools available at the moment.
Server Side Rendering
Recoil claims to be able to support server side rendering, however, it is still not official yet.
Concurrent Mode
Since concurrent mode in React is under experimental, but Recoil is getting ready for it too. Check out the documentation for more details.
Conclusion
In conclusion, I do think Recoil is a promising state management tool that can outrun Redux in the future. Especially it reduces a lot of boiler-plating compared to Redux, in which action/reducer pair (sometimes selector) have to be created just to achieve a functionality.
I am really looking forward to Recoil. If you found something that is worth highlighting for the comparison, please don’t hesitate to leave your thoughts in the opinion below.
Note that any comparisons vs Redux should be done using our new official Redux Toolkit package (https://redux-toolkit.js.org), which is our recommended approach for writing Redux logic. Also, the Redux Style Guide specifically recommends using the “ducks” pattern for organizing Redux logic for a given feature into a single file (https://redux.js.org/style-guide/style-guide#structure-files-as-feature-folders-or-ducks ) .
Hi Mark, thank you for pointing out. I will keep updating the post from time to time to keep it up to date.