iTranslated by AI
[TS] Getting Started with Recoil
Introduction
This time, I'm introducing Recoil (+ my own study notes).
Recoil is a comprehensive state management library for React developed by Facebook.
As mentioned in the article below, it is also used for state management in Zenn, making it a library that is currently expanding its market share.
Since implementation examples in JavaScript are common, I will introduce examples written using TypeScript this time.
What is Recoil anyway?
Before introducing the implementation, let's talk about Recoil.
Recoil is a state management library announced by Facebook in May 2020.
Below is the official page:
While Redux often feels like it's a head above the rest among similar state management libraries, you'll find it's quite different once you actually use it.
In particular, the philosophy regarding "whether or not to centralize the data store" is significantly different, and this characteristic is directly reflected in its usability.
It also has great affinity with React Hooks, using similar syntax and patterns to useState. Therefore, if you are already developing primarily with Functional Components + Hooks, you should be able to understand it easily.
Note: It is currently still an Experimental repository.
Considering future support and other factors, it might be a bit early to integrate it heavily into production products.
Implementation Sample
The following is a sample of the app implemented this time.
It is a common TODO app where you can add and search for tasks.
Version Information
- react@17.0.1
- react-dom@17.0.1
- recoil@0.1.2
Implementation
Implementation is carried out according to the following steps.
Creating the Project
Create the project using the following command.
I named the project recoil-ts-sample.
npx create-react-app recoil-ts-sample --template typescript
cd recoil-ts-sample
Installing Recoil
Install Recoil.
yarn add recoil
Creating Types
First, create the type for the "tasks" handled in this app.
The source file is src/types/Todo.ts.
This time, I defined a simple Todo type that only has a title.
type Todo = {
title : string;
}
export default Todo;
Creating Atoms
Next, we will create an Atom.
An Atom is a data store in Recoil and is declared using atom().
You pass an object of type AtomOptions to atom().
AtomOptions is defined as follows:
// atom.d.ts
export interface AtomOptions<T> {
key: NodeKey;
default: RecoilValue<T> | Promise<T> | T;
effects_UNSTABLE?: ReadonlyArray<AtomEffect<T>>;
dangerouslyAllowMutability?: boolean;
}
Only key and default are required.
key is a string that must be unique across the entire application, and default is the initial value for the state you want to manage as an Atom.
For example, to declare the "task list" to be displayed in the app as an Atom, it would look like this:
import { atom } from "recoil";
import Todo from '../types/Todo';
export const todoListState = atom<Todo[]>({
// key is "todoList"
key: "todoList",
// Declare an array with 3 tasks as the initial value
default: [
{title: "one"},
{title: "two"},
{title: "three"},
],
});
Similarly, declare the states for the "field for the task name to be added" and the "search string field" at the top of the screen as Atoms.
import { atom } from "recoil";
export const todoTitleFormState = atom<string>({
key: "todoTitleForm",
default: '',
});
import { atom } from "recoil";
export const searchTextFormState = atom<string>({
key: "searchTextForm",
default: '',
});
It might be easier to understand Atom declarations if you think of them as being similar to a Store in Redux.
Creating Selectors
Similarly, create a Selector.
A Selector returns the result of some calculation, transformation, or side effect using Atom values.
In this app example, the "task list actually displayed on the screen" is the value retrieved using a Selector.
This is because the tasks actually shown on the screen represent only those containing the "string entered in the search field" filtered from the "total tasks".
A Selector is declared using selector().
You need to pass an object of type ReadOnlySelectorOptions as an argument.
The ReadOnlySelectorOptions type is as follows:
export interface ReadOnlySelectorOptions<T> {
key: string;
get: (opts: { get: GetRecoilValue }) => Promise<T> | RecoilValue<T> | T;
dangerouslyAllowMutability?: boolean;
}
Like an Atom, it has a unique key.
Additionally, it has a get property that defines "what kind of value to retrieve".
get is a function that receives an object with a property named get of type GetRecoilValue as an argument (it's a bit confusing because the property name is the same...).
The following is the Selector we actually created:
import { selector } from "recoil";
import { todoListState } from '../atoms/TodoListAtom';
import { searchTextFormState } from '../atoms/SearchTextFormAtom';
import Todo from '../types/Todo';
export const searchedTodoListSelector = selector<Todo[]>({
key: "searchedTodoListSelector",
// get is a function that takes { get } as an argument
get: ({ get }) => {
// Use the get argument to retrieve the latest value from the Atom (task list)
const todoList: Todo[] = get(todoListState);
// Similarly, retrieve the string from the search field
const searchText: string = get(searchTextFormState);
// If there is input in the search field, return only the tasks matching that condition
return searchText? todoList.filter((t) => t.title.includes(searchText)) : todoList;
},
});
Declaring the Root
Now we are ready to manage state with Recoil.
To manage state with Recoil, you need to wrap the parts you want to manage with <RecoilRoot>.
It's similar to wrapping with <Provider> in Redux.
Let's wrap App with RecoilRoot in index.tsx.
import React from "react";
import ReactDOM from "react-dom";
import { RecoilRoot } from "recoil";
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>,
document.getElementById("root")
);
// ...omitted below
Retrieving values from Atoms or Selectors
Now let's actually try retrieving values from an Atom or Selector within a component.
To retrieve values, use useRecoilValue().
Pass the declared Atom or Selector as an argument to useRecoilValue().
It will return the latest value of that Atom or Selector.
For example, TodoList.tsx, which displays the "task list", looks like this:
import { useRecoilValue } from "recoil";
import { searchedTodoListSelector } from "../selectors/SearchedTodoListSelector";
import Todo from "../types/Todo";
const TodoList: React.FC = () => {
// Pass searchedTodoListSelector to useRecoilValue
// The returned value is Todo[], as defined in the get() of searchedTodoListSelector
const list: Todo[] = useRecoilValue(searchedTodoListSelector);
return (
<div>
<p>Task List</p>
<ul>
{list.map((todo: Todo, i: number) => {
return <li key={`${todo.title}_${i}`}>{todo.title}</li>;
})}
</ul>
</div>
);
};
export default TodoList;
Changing Atom values
In Redux, when the value of the Store is changed by dispatching an Action, the referring components are re-rendered.
In Recoil as well, changing an Atom value automatically reflects the change in the referring Selectors and components.
To change an Atom value, you pass the Atom as an argument to useSetRecoilState().
It returns a function of type SetterOrUpdater<T>.
This is like a Setter function for the Atom value, and by updating the Atom through this function, the aforementioned recalculation process is performed.
For example, TitleForm.tsx, which is the component for the "field to enter the task name to be added", looks like this:
import { useCallback } from "react";
import { useRecoilValue, useSetRecoilState, SetterOrUpdater } from "recoil";
import { todoTitleFormState } from "../atoms/TodoTitleFormAtom";
const TitleForm: React.FC = () => {
// Retrieve the value of todoTitleFormState with useRecoilValue
const todoTitleFormValue: string = useRecoilValue(todoTitleFormState);
// Retrieve the Setter function to update the value of todoTitleFormState with useSetRecoilState
const setTodoTitleFormValue: SetterOrUpdater<string> = useSetRecoilState(
todoTitleFormState
);
// onChange processing for the text field
const onChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
// Pass the value to be updated to the previously obtained setTodoTitleFormValue and execute
setTodoTitleFormValue(event.target.value);
},
[setTodoTitleFormValue]
);
return (
<label>
タスク名:
<input
type="text"
value={todoTitleFormValue}
onChange={onChange}
name="title"
style={{ margin: 12 }}
/>
</label>
);
};
export default TitleForm;
Similarly, let's create the component for the "field to enter the search string".
import { useCallback } from "react";
import { useRecoilValue, useSetRecoilState, SetterOrUpdater } from "recoil";
import { searchTextFormState } from "../atoms/SearchTextFormAtom";
const SearchForm: React.FC = () => {
const searchTextFormValue: string = useRecoilValue(searchTextFormState);
const setSearchTextFormValue: SetterOrUpdater<string> = useSetRecoilState(
searchTextFormState
);
const onChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTextFormValue(event.target.value);
},
[setSearchTextFormValue]
);
return (
<label>
検索:
<input
type="text"
value={searchTextFormValue}
onChange={onChange}
name="title"
style={{ margin: 12 }}
/>
</label>
);
};
export default SearchForm;
Also, let's prepare a button component to "add a task".
import { useCallback } from "react";
import { useRecoilValue, useSetRecoilState, SetterOrUpdater } from "recoil";
import { todoTitleFormState } from "../atoms/TodoTitleFormAtom";
import { todoListState } from "../atoms/TodoListAtom";
import Todo from "../types/Todo";
const AddButton: React.FC = () => {
const todoList: Todo[] = useRecoilValue(todoListState);
const todoTitleFormValue: string = useRecoilValue(todoTitleFormState);
const setTodoList: SetterOrUpdater<Todo[]> = useSetRecoilState(todoListState);
const setTitleFormValue: SetterOrUpdater<string> = useSetRecoilState(
todoTitleFormState
);
const onClick = useCallback(() => {
setTodoList([...todoList, { title: todoTitleFormValue }]);
// Clear the input field after adding a task
setTitleFormValue("");
}, [todoList, todoTitleFormValue, setTodoList, setTitleFormValue]);
return <button onClick={onClick}>追加</button>;
};
export default AddButton;
Editing App
Now the creation of the necessary components is complete. Let's rewrite App.tsx as follows and actually use the set of components that use Recoil.
import "./App.css";
import TitleForm from "./component/TitleForm";
import AddButton from "./component/AddButton";
import TodoList from "./component/TodoList";
import SearchForm from "./component/SearchForm";
const App: React.FC = () => {
return (
<div style={{ margin: 12 }}>
<div>
<TitleForm />
<AddButton />
</div>
<SearchForm />
<TodoList />
</div>
);
};
export default App;
If it starts up without any problems using the following command, an app like the sample introduced at the beginning should launch.
yarn start
What was created this time
The set of source files above has been uploaded to the following GitHub repository. Please refer to it if you want to follow the code while running the application.
Summary
In this article, I introduced Recoil by creating a simple task management app using TypeScript.
While I usually develop using Redux, I felt that Recoil is easier to understand because it is simpler and more intuitive.
Also, the fact that its usage feels like Hooks makes it feel like a modern React app, which I think is a strength of a later-released library.
Although it is still treated as Experimental, and its usage or support may change significantly in the future, personally, I felt it is a beneficial library that is likely to grow from now on.
Discussion