Managing complex state in React can sometimes be hard, which is why some of us use Redux or similar libraries like MobX to manage state in React. Recoil is another state management library that is closely modeled towards React’s Hooks API. It allows you to define shared state as atoms, and computed state which it refers to as selectors. If you want to learn about the limitation the team at Facebook faced and how they tried to solve it with recoil, you can watch this video.
One important note: Although many companies including Facebook are using Recoil, it is technically in an experimental state, and its API and functionality may change.
In this post, I'll show you how to switch from Redux to Recoil and along the way compare the differences. I'll be working with the TodoMVC example from Redux's GitHub repository. You can download the zip file using this link I made for you 😉. Here's how the app works:
Setting Up Recoil
The first step to use any JavaScript library is to add it to the project. You can add a reference using the HTML <script>
tag, or install it via npm. Since you have downloaded an npm-style project, install Recoil by running npm install recoil
or yarn add recoil
.
Similar to using Redux where we wrap our root component with the <Provider />
, we're going to replace that with <RecoilRoot />
so that Recoil state is available to the child components.
Open src/index.js and import the RecoilRoot
module.
import { RecoilRoot } from "recoil";
Then update the render function as follows:
render(
<RecoilRoot>
<App />
</RecoilRoot>,
document.getElementById("root")
);
Defining and Updating State
To represent a piece of state, you declare what is called an atom
. Since we want to store a list of todos, we will create an atom with a default or initial state. Create a new recoil directory and add a new file named todos with the following content.
import { atom } from "recoil";
export const todos = atom({
key: "todos",
default: [],
});
Now open component/Header.js and update it with this code:
import React from "react";
import TodoTextInput from "./TodoTextInput";
import { useSetRecoilState } from "recoil";
import { todos } from "../recoil/todos";
const Header = () => {
const setTodos = useSetRecoilState(todos);
const save = (text) => {
if (text.length !== 0) {
setTodos((todos) => [
...todos,
{
id: Date.now(),
text,
completed: false,
},
]);
}
};
return (
<header className="header">
<h1>todos</h1>
<TodoTextInput
newTodo
onSave={save}
placeholder="What needs to be done?"
/>
</header>
);
};
export default Header;
This component displays a text input to collect new todos and save them. To add a new todo, we need a function that will update the contents of the todos
state. We used the useSetRecoilState()
hook to get a setter function which is used in the save()
function. On line 11, we used the updater form of the setter function so that we can create a new list based on the old todos. That is all we need to do to be able to collect and store todo items.
If you compare this to Redux, you would need to create action creators and reducers to update a piece of state, then connect the component to Redux store and dispatch actions. In Recoil, you define an atom to hold data, then use hooks API to interact with that data. If you're new to React and understand the hooks API, it should be quick to grasp Recoil because it's closely modeled to React's API, unlike Redux where you'd need to understand its style of unidirectional data flow.
Derived State
The next section in the app to update is the <MainSection />
component. It renders an input to mark all todos as completed, and also two extra components which we'll get to later. So, open componenrs/MainSection.js and update it with the code below:
import React from "react";
import { useRecoilValue, useRecoilState } from "recoil";
import Footer from "./Footer";
import VisibleTodoList from "../containers/VisibleTodoList";
import { completedTodoCount, todos } from "../recoil/todos";
const MainSection = () => {
const completedCount = useRecoilValue(completedTodoCount);
const [todoList, setTodoList] = useRecoilState(todos);
const todosCount = todoList.length;
const clearCompleted = () => {
setTodoList((previousTodos) =>
previousTodos.filter((todo) => todo.completed === false)
);
};
const completeAllTodos = () =>
setTodoList((previousTodos) => {
const areAllMarked = previousTodos.every((todo) => todo.completed);
return previousTodos.map((todo) => ({
...todo,
completed: !areAllMarked,
}));
});
return (
<section className="main">
{!!todosCount && (
<span>
<input
className="toggle-all"
type="checkbox"
checked={completedCount === todosCount}
readOnly
/>
<label onClick={completeAllTodos} />
</span>
)}
<VisibleTodoList />
{!!todosCount && (
<Footer
completedCount={completedCount}
activeCount={todosCount - completedCount}
onClearCompleted={clearCompleted}
/>
)}
</section>
);
};
export default MainSection;
What we did here is that instead of connecting to Redux and calling mapStateToProps
and mapDispatchToProps
, we used two Recoil hooks, which are useRecoilValue
and useRecoilState
. The useRecoilValue()
function is used to read the content of a state; in our case it's completedTodoCount
. We want to get the todos
state and also be able to update it. For that we use useRecoilState()
to read todos
and get a function to update it. We have two functions, clearCompleted()
and completeAllTodos()
, that are used to update the state.
We need to define the completedTodoCount
state. This should be computed from the todos
state. For that, we're going to create what's called selector in Recoil. Open recoil/todos.js and import selector from the Recoil package.
import { atom, selector } from "recoil";
Then define the selector as you see below:
export const completedTodoCount = selector({
key: "completedTodoCount",
get: ({ get }) => {
const list = get(todos);
return list.reduce(
(count, todo) => (todo.completed ? count + 1 : count),
0
);
},
});
To define a selector, you call the selector()
function with an object which contains the name for the state and a get()
function that will compute and return a value. This function receives an object that has a get()
function that can be used to retrieve data from other atoms or selectors.
Filtering Todos
At this point, I've covered most basics of Recoil and you can see how it's different from Redux but closely modeled towards React's Hooks API. The rest of this post will just be adding code to make the app fully functional using Recoil.
The next component we'll work on is the <FilterLink />
component. Open containers/FilterLink.js and update the file with the code below:
import React from "react";
import { useRecoilState } from "recoil";
import Link from "../components/Link";
import { visibilityFilter } from "../recoil/todos";
export default ({ filter, children }) => {
const [visibility, setVisibilityFilter] = useRecoilState(visibilityFilter);
const setFilter = () => setVisibilityFilter(filter);
return (
<Link
active={filter === visibility}
setFilter={setFilter}
children={children}
/>
);
};
Here we're rendering the <Link />
component which will render input used to select how to filter the todos that'll be displayed. We used a new state which we didn't create yet, so we'll add that in. Open recoil/todos.js and add the function below:
import {
SHOW_ALL,
SHOW_COMPLETED,
SHOW_ACTIVE,
} from "../constants/TodoFilters";
export const visibilityFilter = atom({
key: "visibilityFilter",
default: SHOW_ALL,
});
Display Todos
The next thing to do is to display the todos based on the filter that's set. For that, we'll add a new selector and update the <VisibleTodoList />
component. While you still have recoil/todos.js open, add the selector below to it.
export const filteredTodos = selector({
key: "filteredTodos",
get: ({ get }) => {
const filter = get(visibilityFilter);
const list = get(todos);
switch (filter) {
case SHOW_COMPLETED:
return list.filter((t) => t.completed);
case SHOW_ACTIVE:
return list.filter((t) => !t.completed);
default:
return list;
}
},
});
Open containers/VisibleTodoList.js and update the file with the code below:
import React from "react";
import TodoList from "../components/TodoList";
import { filteredTodos, todos } from "../recoil/todos";
import { useRecoilValue, useSetRecoilState } from "recoil";
const VisibleTodoList = () => {
const filteredTodoList = useRecoilValue(filteredTodos);
const setTodos = useSetRecoilState(todos);
const completeTodo = (todoId) => {
setTodos((previousTodos) =>
previousTodos.map((todo) =>
todo.id === todoId ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = (todoId) => {
setTodos((previousTodos) =>
previousTodos.filter((todo) => todo.id !== todoId)
);
};
const editTodo = (todoId, text) => {
setTodos((previousTodos) =>
previousTodos.map((todo) =>
todo.id === todoId ? { ...todo, text } : todo
)
);
};
return (
<TodoList
filteredTodos={filteredTodoList}
actions={{ completeTodo, deleteTodo, editTodo }}
/>
);
};
export default VisibleTodoList;
Here we added three functions to delete a todo, update it, or mark it as completed. We can consider these functions as a combination of actions and reducer functions in Redux. I decided to put the functions in the same file as the component that needs it, but you can extract them into a separate file if you wish.
At this point, we've updated the app to use Recoil instead of Redux. The last thing to do is to update components/App.js. Open this file and change the import statement for the <Header />
and <MainSection />
components.
import Header from "./Header";
import MainSection from "./MainSection";
And there you have it, a todo app updated from using redux to recoil.
Conclusion
Moving this app from Redux to Recoil was less complicated than I'd imagined. I guess this won't be the case for all your apps, based on how you designed your Redux state and a few other factors. But I think it's fairly easy to use for new apps because it's modeled after the React API that you're familiar with.
You can learn more about Recoil on recoiljs.org. You can find the completed app with source code on GitHub.
Originally posted on Telerik
Top comments (0)