話題の「Recoil」を使ってTodoアプリ作ってみた with TypeScript

## はじめに

Facebook公式から新しいステート管理のためのライブラリが公開されました。

その名もRecoilです。

特徴としては

- ミニマム
- Hooksではお馴染みのuseStateと同じようなAPI
- パフォーマンス問題が解決できる(Viewの再レンダリングを抑える)

が挙げられるかと思います。

早速Todoアプリを作ってみましょう。


## 環境構築
```bash
$ npx create-react-app todo-recoil --typescript
$ cd todo-recoil
$ yarn add recoil
```

## 型定義しておく
意気揚々とTSで始めようとしましたが、型定義ファイルは2020/05/15 10時現在は無いようです。

ですが、TS supportがIssueで上がっていて、既に予定しているようです。

https://github.com/facebookexperimental/Recoil/issues/6

有志が型定義ファイルを作ってくださったみたいなので、今回はとりあえずそれを利用させていただきます。

https://github.com/csantos42/DefinitelyTyped/blob/recoil-types/types/recoil/index.d.ts

<details><summary>**型定義ファイル(押すと開くよ)**</summary><div>


```ts:src/react-app-env.d.ts
/// <reference types="react-scripts" />

// Type definitions for recoil 0.0
// Project: https://github.com/facebookexperimental/recoil#readme
// Definitions by: Christian Santos <https://github.com/csantos42>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// Minimum TypeScript Version: 3.7

/// <reference types="react" />

declare module 'recoil' {
    // Nominal Classes
    export import DefaultValue = __Recoil.DefaultValue;

    // Components
    export import RecoilRoot = __Recoil.RecoilRoot;

    // RecoilValues (aka Recoil state)
    export import atom = __Recoil.atom;
    export import selector = __Recoil.selector;

    // Hooks for working with Recoil state
    export import useRecoilValue = __Recoil.useRecoilValue;
    export import useRecoilValueLoadable = __Recoil.useRecoilValueLoadable;
    export import useRecoilState = __Recoil.useRecoilState;
    export import useRecoilStateLoadable = __Recoil.useRecoilStateLoadable;
    export import useSetRecoilState = __Recoil.useSetRecoilState;
    export import useResetRecoilState = __Recoil.useResetRecoilState;
    export import useRecoilCallback = __Recoil.useRecoilCallback;

    // Other
    export import isRecoilValue = __Recoil.isRecoilValue;
}

declare module 'recoil/utils' {
    // Convenience RecoilValues
    export import atomFamily = __Recoil.atomFamily;
}

declare namespace __Recoil {
    // Recoil_Node.js
    export class DefaultValue {}

    // Recoil_State.js
    type NodeKey = string;

    // Recoil_RecoilValue.js
    class AbstractRecoilValue<T> {
        tag: 'Writeable';
        key: NodeKey;
        constructor(newKey: NodeKey);
    }

    class AbstractRecoilValueReadonly<T> {
        tag: 'Readonly';
        key: NodeKey;
        constructor(newKey: NodeKey);
    }

    class RecoilState<T> extends AbstractRecoilValue<T> {}

    class RecoilValueReadOnly<T> extends AbstractRecoilValueReadonly<T> {}

    type RecoilValue<T> = RecoilValueReadOnly<T> | RecoilState<T>;

    export function isRecoilValue(val: unknown): val is RecoilValue<any>;

    // Recoil_State.js
    type AtomValues = Map<NodeKey, Loadable<any>>;
    type ComponentCallback = (state: TreeState) => void;
    type TreeState = Readonly<{
        // Information about the TreeState itself:
        isSnapshot: boolean;
        transactionMetadata: object;
        dirtyAtoms: Set<NodeKey>;

        // ATOMS
        atomValues: AtomValues;
        nonvalidatedAtoms: Map<NodeKey, unknown>;

        // NODE GRAPH -- will soon move to StoreState
        // Upstream Node dependencies
        nodeDeps: Map<NodeKey, Set<NodeKey>>;

        // Downstream Node subscriptions
        nodeToNodeSubscriptions: Map<NodeKey, Set<NodeKey>>;
        nodeToComponentSubscriptions: Map<NodeKey, Map<number, [string, ComponentCallback]>>;
    }>;

    // Recoil_Loadable.js
    type ResolvedLoadablePromiseInfo<T> = Readonly<{
        value: T;
        upstreamState__INTERNAL_DO_NOT_USE?: TreeState;
    }>;

    type LoadablePromise<T> = Promise<ResolvedLoadablePromiseInfo<T>>;

    type Accessors<T> = Readonly<{
        // Attempt to get the value.
        // If there's an error, throw an error.  If it's still loading, throw a Promise
        // This is useful for composing with React Suspense or in a Recoil Selector.
        getValue: () => T;

        toPromise: () => LoadablePromise<T>;

        // Convenience accessors
        valueMaybe: () => T | void;
        valueOrThrow: () => T;
        errorMaybe: () => Error | void;
        errorOrThrow: () => Error;
        promiseMaybe: () => Promise<T> | void;
        promiseOrThrow: () => Promise<T>;

        map: <T, S>(map: (val: T) => Promise<S> | S) => Loadable<S>;
    }>;

    export type Loadable<T> =
        | Readonly<Accessors<T> & { state: 'hasValue'; contents: T }>
        | Readonly<Accessors<T> & { state: 'hasError'; contents: Error }>
        | Readonly<
        Accessors<T> & {
        state: 'loading';
        contents: LoadablePromise<T>;
    }
        >;

    // Recoil_RecoilRoot.react.js
    type RecoilRootProps = {
        initializeState?: (options: {
            set: <T>(recoilVal: RecoilValue<T>, newVal: T) => void;
            setUnvalidatedAtomValues: (atomMap: Map<string, unknown>) => void;
        }) => void;
    };

    export const RecoilRoot: React.FC<RecoilRootProps>;

    // Recoil_atom.js
    type AtomOptions<T> = Readonly<{
        key: NodeKey;
        default: RecoilValue<T> | Promise<T> | T;
        // persistence_UNSTABLE?: PersistenceSettings<T>,
        dangerouslyAllowMutability?: boolean;
    }>;

    export function atom<T>(options: AtomOptions<T>): RecoilState<T>;

    // Recoil_selector.js
    type GetRecoilValue = <T>(recoilVal: RecoilValue<T>) => T;
    type SetRecoilState = <T>(
        recoilVal: RecoilState<T>,
        newVal: T | DefaultValue | ((prevValue: T) => T | DefaultValue),
    ) => void;
    type ResetRecoilState = <T>(recoilVal: RecoilState<T>) => void;

    type ReadOnlySelectorOptions<T> = {
        key: string;
        get: (opts: { get: GetRecoilValue }) => Promise<T> | RecoilValue<T> | T;

        // cacheImplementation_UNSTABLE?: CacheImplementation<Loadable<T>>,
        dangerouslyAllowMutability?: boolean;
    };

    type ReadWriteSelectorOptions<T> = ReadOnlySelectorOptions<T> & {
        set: (
            opts: // FIXME: these types are not working
                {
                    set: SetRecoilState;
                    get: GetRecoilValue;
                    reset: ResetRecoilState;
                },
            newValue: T | DefaultValue,
        ) => void;
    };

    export function selector<T>(options: ReadOnlySelectorOptions<T>): RecoilValueReadOnly<T>;
    export function selector<T>(options: ReadWriteSelectorOptions<T>): RecoilState<T>;

    // Recoil_Hooks.js
    type SetterOrUpdater<T> = (valOrUpdater: ((currVal: T) => T) | T) => void;
    type Resetter = () => void;
    type CallbackInterface = Readonly<{
        getPromise: <T>(recoilVal: RecoilValue<T>) => Promise<T>;
        getLoadable: <T>(recoilVal: RecoilValue<T>) => Loadable<T>;
        set: <T>(recoilVal: RecoilState<T>, valOrUpdater: ((currVal: T) => T) | T) => void;
        reset: <T>(recoilVal: RecoilState<T>) => void;
    }>;

    export function useRecoilValue<T>(recoilValue: RecoilValue<T>): T;
    export function useRecoilValueLoadable<T>(recoilValue: RecoilValue<T>): Loadable<T>;
    export function useRecoilState<T>(recoilState: RecoilState<T>): [T, SetterOrUpdater<T>];
    export function useRecoilStateLoadable<T>(recoilState: RecoilState<T>): [Loadable<T>, SetterOrUpdater<T>];
    export function useSetRecoilState<T>(recoilState: RecoilState<T>): SetterOrUpdater<T>;
    export function useResetRecoilState<T>(recoilState: RecoilState<T>): Resetter;
    export function useRecoilCallback<Args extends ReadonlyArray<unknown>, Return>(
        fn: (interface: CallbackInterface, ...args: Args) => Return,
        deps?: ReadonlyArray<unknown>,
    ): (...args: Args) => Return;

    // Recoil_atomFamily.js
    type Primitive = void | null | boolean | number | string;
    type AtomFamilyParameter =
        | Primitive
        | ReadonlyArray<AtomFamilyParameter>
        | Readonly<{ [k: string]: AtomFamilyParameter }>;
    type AtomFamilyOptions<T, P extends AtomFamilyParameter> = Readonly<
        AtomOptions<T> & {
        default:
            | RecoilValue<T>
            | Promise<T>
            | T
            | ((param: any /*FIXME*/) => T | RecoilValue<T> | Promise<T>)
            | any; // FIXME
    }
        >;

    function atomFamily<T, P extends AtomFamilyParameter>(
        options: AtomFamilyOptions<T, P>,
    ): (param: P) => RecoilState<T>;
}
```


</div></details>



## RecoilRootを設置する
Recoilでステート管理を使用するコンポーネントは、親ツリーのどこかに `RecoilRoot` を配置する必要があります！

これを配置するのに適した場所は、ルートコンポーネントの中です。


`src/App.tsx` にRecoilRootをおきます。

```tsx:src/App.tsx
import React from 'react';
import {TaskList} from './Task';

import {
    RecoilRoot,
} from 'recoil';


function App() {
    return (
        <RecoilRoot>
            <TaskList/>
        </RecoilRoot>
    );
}

export default App;
```

## Atomを作る
Atomとはデータストアです。

keyは**一意**にする必要があります。
defaultには初期値を設定しておきましょう。

```bash
$ mkdir src/atoms
$ touch src/atoms/Task.ts
```

```ts:src/Atoms/Task.ts
import {atom} from 'recoil';

export interface Task {
    title: string;
    completed: boolean;
}

const initialTasks: Task[] = [];

export const taskState = atom({
    key: 'task',
    default: initialTasks,
})
```

## Todo入力部分を作る
Recoilのhookがいくつかあるのですが、代表的なものが `useRecoilState` です。
useStateと同じ感じのAPIでatomを引数にとり、setterとgetterを戻り値にします。

```ts
const [tasks, setTasks] = useRecoilState(taskState);
```

これの兄弟に `useRecoilValue` と `useSetRecoilState` があります。
名前から予想できると思いますが、それぞれgetterとsetterが独立したものです。

以下は先ほどのコードと同意です。どちらかのアクションしか無い場合はunused等で怒られないよう適切に使っていきましょう。

```ts
// const [tasks, setTasks] = useRecoilState(taskState);
const tasks = useRecoilValue(taskState);
const setTasks = useSetRecoilState(taskState);
```

useRecoilStateの使い方を踏まえて、Todo入力部分を作ってみましょう。

```tsx:src/Task.tsx
import React, {useState} from "react";
import {useSetRecoilState} from "recoil";
import {taskState} from "./atoms/Task";

const TaskInput = () => {
    const [title, setTitle] = useState('');
    const setTasks = useSetRecoilState(taskState);
    const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        setTitle(event.target.value)
    }
    const onClick = () => {
        setTasks(t => {
            return [...t, {title, completed: false}]
        })
        setTitle('')
    }
    return (
        <div>
            <label>
                タスク名
                <input type="text" value={title} onChange={onChange}/>
            </label>
            <button onClick={onClick}>登録</button>
        </div>
    )
}

export const TaskList = () => {
    return (
        <>
            <TaskInput/>
        </>
    )
}
```

## Todoのリスト部分を作る

`const tasks = useRecoilValue(taskState);` でTodoのリストは取得できます。

```diff:src/Todo.tsx
+ import {useRecoilValue, useSetRecoilState} from "recoil";

.....

export const TaskList = () => {
+   const tasks = useRecoilValue(taskState);
+
    return (
        <>
            <TaskInput/>
+            <ul>
+                {tasks.map((t, index) => {
+                    return <TaskItem task={t} index={index} key={index}/>
+                })}
+            </ul>
        </>
    )
}

```

## Todoの表示部分を作る

注意点は `useRecoilState` や `useRecoilValue` で取り出す値はread onlyな点です。

最初実装した時に以下のようなコードを書いていました。

```ts:ダメなコード例
tasks[index].completed = !tasks[index].completed
```

しかし、ステートはread onlyで再代入不可なので、sliceを使って新たな配列を作り、そこに新しいオブジェクトを挿入する感じにしています。

```tsx:src/App.tsx
....

interface TaskItemProps {
    task: Task;
    index: number;
}

const removeTasksAtIndex = (tasks: Task[], index: number) => {
    return [...tasks.slice(0, index), ...tasks.slice(index + 1)]
}

const replaceTasksAtIndex = (tasks: Task[], index: number, newTask: Task) => {
    return [...tasks.slice(0, index), newTask, ...tasks.slice(index + 1)]
}

const TaskItem: FC<TaskItemProps> = ({task, index}) => {
    const [tasks, setTasks] = useRecoilState(taskState);

    const onChange = () => {
        const newTasks = replaceTasksAtIndex(tasks, index, {
            ...task,
            completed: !task.completed
        });
        setTasks(newTasks);
    }

    const onClick = () => {
        const newTasks = removeTasksAtIndex(tasks, index);
        setTasks(newTasks);
    }

    return (
        <li key={index}>
            <input
                type="checkbox"
                checked={task.completed}
                onChange={onChange}
            />
            {task.title}
            <button onClick={onClick}>削除</button>
        </li>
    )
}
....
```

もし、「**どうしても俺はread onlyが嫌なんだ！！！！！**」って方は、atomを作る時に `dangerouslyAllowMutability` を trueにすれば良いです。

まあけど、React公式が`dangerously`というぐらいなので、素直にoffっといた方が良いです。
なぜならReduxで培ってきた、データフローを一方向にするという考えに背く羽目になるからです。

```ts:src/Atom/Tasks.ts
export const taskState = atom({
    key: 'task',
    default: initialTasks,
    dangerouslyAllowMutability: true
})
```

## 完成

はい、こんな感じのしょぼいTodoアプリができました。
この程度だとGlobalなステートの恩恵が全くありませんが、Recoilの入門には丁度良いのかな〜という感じです。
![May-15-2020 13-28-18.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82779/260b316d-69ad-462d-c66e-425cac076e73.gif)

全部のコードが置いてあるリポジトリも置いておきました。

https://github.com/serinuntius/todo-recoil

## まとめ
まだRecoilはexperimentalですが、Recoilは非常に馴染みのあるAPIで、使いやすいと個人的には思います。
Reduxが重すぎると思う方は、今後Recoilを使う機会があるかもしれません。

本当はまだ紹介していない、 `selector` という概念があるのですが、この記事が好評だったら書くかもしれません。

## 参考文献

- https://recoiljs.org/
- https://github.com/facebookexperimental/Recoil