iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🔋

[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.

https://zenn.dev/catnose99/articles/zenn-dev-stack

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:

https://recoiljs.org

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.

recoil_sample

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.

src/types/Todo.ts
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:

src/atoms/TodoListAtom.ts
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.

src/atoms/TodoTitleFormAtom.ts
import { atom } from "recoil";

export const todoTitleFormState = atom<string>({
    key: "todoTitleForm",
    default: '',
});
src/atoms/SearchTextFormAtom.ts
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:

src/selectors/SearchedTodoListSelector.ts
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.

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:

src/component/TodoList.tsx
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:

src/component/TitleForm.tsx
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".

src/component/SearchForm.tsx
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".

src/component/AddButton.tsx
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.

App.tsx
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.

https://github.com/ShutoYamada/recoil-ts-sample

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