React で簡単に状態管理できる Recoil の使い方【 TypeScript 】

ショウヘイ

どうも、ノマドクリエイターのショウヘイ(@shohei_creator)です。

ふーちゃん

ブログアシスタントのふーちゃんです。

React で state (変数の状態)を管理する場合は、 useState や useCotntext ようなフック、または state の管理に関するライブラリを使うことが一般的です。

React で state を管理できるライブラリの代表例は、 Redux です。有用性が高い反面、かなり複雑な構造になっており、高い学習コストを必要とします。

比較的に最近になって、 Facebook によって Recoil というライブラリが開発されました。 Recoil は単純明快な構造になっており、ほどほどのアプリ規模の state の管理に適しています。

この記事では、 React ライブラリの Recoil の使い方について説明します。

目次

React ライブラリの Recoil とは

Recoil は、 React で state を管理するためのライブラリです。 Redux よりも簡単に取り扱うことができて、かつ React のフックとも相性がいいため、今後に人気になると期待されています。

Recoil はの主要な機能は、『 state を格納できる Atoms 』と『 Atoms に格納した state を元にして、関数処理できる Selectors 』です。

あわせて読みたい
Recoil A state management library for React.

React ライブラリの Recoil の基本構文と使い方

React で Recoil を使う時は、まずは npm または yarn などの JavaScript パッケージマネージャーを利用して、 Recoil ライブラリをインストールします。

React でアプリ開発するフォルダのディレクトリにて、下記のコマンドを実行してください。

// npmを使う場合
npm install recoil

// yarnを使う場合
yarn install recoil

上層コンポーネント( App コンポーネントなど)にて、 Recoil ライブラリから RecoilRoot コンポーネントを import しておきます。さらに、 RecoilRoot タグの中に、下層コンポーネントを設置します。

こうすることで、下層コンポーネントで Recoil の機能を使えるようになります。

import React from "react";
import { RecoilRoot} from "recoil";
import { Component} from "./Component"

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

Recoil の atom の設定

Recoil の atom は、 Recoil で管理したい state を格納するための機能です。 state を保持することのみに専念するので、 atom そのものに state を更新する機能はありません。その代わりに、 selector が state の更新を担います。

1 つの state につき、 1 つの atom を用意します。そのため、複数の state を取り扱う場合は、 1 ファイルに atom を集約して書いておくと、あとで改修しやすくなります。

実際に atom を使う前に、 Recoil ライブラリから atom を import しておいてください。

atom メソッドの引数として、オブジェクト形式で key プロパティと default プロパティを設定することで、 state を保持できるようになります。

key の値には、 atom を識別するために必要となる一意な文字列を設定します。基本的には、 atom の代入先の変数名とそろえておくと、コードの可読性が上がります。

default の値には、『 atom の初期の呼び出し』や『 atom のリセット』で参照される初期値を設定します。

import { atom } from "recoil";

// 初期値に0を設定する例
const initialState: number = 0;

export const sampleAtom = atom({
  // atomを呼び出すための一意な文字列
  key: "sampleAtom",
  // 初期値
  default: initialState
});

Recoil の selector の設定

Recoil の selector は、 atom から取得した state を引数にして、関数を実行する機能を持っています。主に、 state を加工した後の返り値を得る目的で使われます。

実際に selector を使う前に、 Recoil ライブラリから selector を import しておいてください。また、 selector で使う atom を代入した変数も import しておいてください。

selector メソッドの引数として、オブジェクト形式で key プロパティと get プロパティを設定できます。 取り扱いやすいように、何かしらの変数に代入しておきましょう。 

key の値には、 selector を識別するために必要となる一意な文字列を設定します。基本的には、 selector 変数名とそろえておくと、コードの可読性が上がります。

get の値には、 get を引数とした関数を設定します。関数内では、 get メソッドの引数として atom 変数名を設定することで、 atom に保持されている state を取得できます。

あとは、 state に対して任意の加工を施したうえで、 return 文で返却します。

import { selector } from "recoil";
import { sampleAtom } from "./atoms";

export const sampleSelector = selector({
  key: "sampleSelector",
  get: ({ get }) => {
    // atomから取得したstateに1を足して返却する処理
    const state: number = get(sampleAtom);
    return state + 1;
  }
});

React ライブラリの Recoil の具体例( todo リスト)

ショウヘイ

React の Recoil の使い方を実例で把握してみましょう、 

Recoil のチュートリアルにならって、簡単な todo リストを作ってみます。

完成形は、下の画像のような感じです。

React ライブラリの recoil を使った todo リスト

React の src フォルダのファイル構成は、以下のとおりです。

src
├ App.tsx
├ TodoItem.tsx
├ TodoItemCreator.tsx
├ TodoList.tsx
├ TodoListFilters.tsx
├ TodoListStats.tsx
├ atoms.ts
├ index.tsx
└ type.ts

コードの内容を理解しやすいように、まずは小規模のファイルから説明していきます。

こちらは、型定義を書いておくための type.ts ファイルです。 1 つ1つの todo オブジェクトに対して提供する Todo 型を定義しています。

// Todoの型定義
export type Todo = {
  // todoに割り振る識別数字
  id: number;
  // todoとして入力する文字列
  text: string;
  // todoに取り組み終わったどうかの識別
  isComplete: boolean;
};

atom をまとめて書いておくための atoms.ts ファイルです。

はじめに、 Recoil ライブラリから atom メソッドを import します。ついでに、 type.ts ファイルから、 Todo 型も import しておきます。

todoList オブジェクトは、 todo リストが初めて表示された時の初期値として用意しています。

1 つ目の atom の todoListState は、 todo リスト本体です。 default には、 todoList を設定しておきます。

2 つ目の atom の todoListFilterState は、 todo リストの表示条件です。表示する todo を絞り込むフィルター機能のために使います。 default の文字列は「 Show All 」としておきます。

import { atom } from "recoil";
import { Todo } from "./type";

// todoリストの初期値
const todoList: Todo[] = [
  { id: 10000, text: "牛乳と卵を買ってくる", isComplete: true },
  { id: 10001, text: "燃えるごみを捨てる", isComplete: false },
  { id: 10002, text: "子供に今月分のお小遣いを渡す", isComplete: false }
];

// todoリストのstate
export const todoListState = atom({
  key: "todoListState",
  default: todoList
});

// todoリストの表示条件のstate
export const todoListFilterState = atom({
  key: "todoListFilterState",
  default: "Show All"
});

selector をまとめて書いておくための selectors.ts ファイルです。

はじめに、 Recoil ライブラリから selector メソッドを import します。また、それぞれの selector で使う予定の todoListState と todoListFilterState を atoms.ts ファイルから import しておきます。

1 つ目の selector の filteredTodoListState は、表示条件によってフィルタリングした結果( todo リスト)を返却します。 2 つの atom から『 todo リスト本体』と『表示条件の文字列』を取得した後、 switch 文による case 分岐を活用して、『表示条件の文字列』 に対応する絞り込みをおこないます。

2 つ目の selector の todoListStatsState は、 todo リストについての統計情報を返却します。返却内容は、 todo の合計数・完了した個数・未完了の個数・合計数に対する完了個数の百分率です。

import { selector } from "recoil";
import { todoListState, todoListFilterState } from "./atoms";

// filterメソッドによって絞り込んだtodoを返却する
export const filteredTodoListState = selector({
  key: "filteredTodoListState ",
  get: ({ get }) => {
    const filter = get(todoListFilterState);
    const list = get(todoListState);

    switch (filter) {
      case "Show Completed":
        return list.filter((todo) => todo.isComplete);
      case "Show Uncompleted":
        return list.filter((todo) => !todo.isComplete);
      default:
        return list;
    }
  }
});

// todoリストについての統計情報を返却する
export const todoListStatsState = selector({
  key: "todoListStatsState",
  get: ({ get }) => {
    const todoList = get(todoListState);
    const totalNum = todoList.length;
    const totalCompletedNum = todoList.filter((todo) => todo.isComplete).length;
    const totalUncompletedNum = totalNum - totalCompletedNum;
    const percentCompleted =
      totalNum === 0 ? 0 : (totalCompletedNum / totalNum) * 100;

    return {
      totalNum,
      totalCompletedNum,
      totalUncompletedNum,
      percentCompleted
    };
  }
});

小規模のファイルの説明が終わったので、上層コンポーネントから順に説明していきます。

まずは、 index.tsx ファイルです。 bootstarap 導入用の css ファイルの import を除けば、 create-react-app コマンドで生成した初期状態と同じです。

import { render } from "react-dom";
import App from "./App";
import "bootstrap/dist/css/bootstrap.min.css";

const rootElement = document.getElementById("root");
render(<App />, rootElement);

App.tsx ファイルです。

TodoList コンポーネント以下で Recoil の機能を使いたいので、 RecoilRoot コンポーネントの間に TodoList コンポーネントを設置します。 

import { RecoilRoot } from "recoil";
import { TodoList } from "./TodoList";
import { Container } from "react-bootstrap";

export default function App() {
  return (
    <RecoilRoot>
      <Container className="mt-4 mb-4">
        <TodoList />
      </Container>
    </RecoilRoot>
  );
}

TodoList.tsx ファイルです。 todo リストを構成する各種コンポーネントの大枠の役割を務めます。

まずは、 Recoil の useRecoilValue フックを使って、 selector の filteredTodoListState から『フィルタリングずみ todo リスト(初期状態では全表示)』を取得します。

あとは、 todo リストを機能させるために必要なコンポーネントを並べておきます。

import React from "react";
import { useRecoilValue } from "recoil";
import { filteredTodoListState } from "./selector";
import { TodoListStats } from "./TodoListStats";
import { TodoListFilters } from "./TodoListFilters";
import { TodoItemCreator } from "./TodoItemCreator";
import { TodoItem } from "./TodoItem";
import { ListGroup } from "react-bootstrap";

export const TodoList: React.VFC = () => {
  // useRecoilValueフックを使って、selectorのフィルタリングずみtodoリストを取得
  const filterdTodoList = useRecoilValue(filteredTodoListState);

  return (
    <>
      <TodoListStats />
      <TodoItemCreator />

      <div style={{ display: "flex", justifyContent: "space-between" }}>
        <p>todoリスト</p>
        <TodoListFilters />
      </div>

      <ListGroup>
        {filterdTodoList.map((todo) => (
          <ListGroup.Item key={todo.id}>
            <TodoItem todo={todo} />
          </ListGroup.Item>
        ))}
      </ListGroup>
    </>
  );
};

TodoListStats.tsx ファイルです。 todo リストについての統計情報を表示する役割を務めます。

まずは、 Recoil の useRecoilValue フックを使って、 selector の todoListStatsState から各種の変数を分割代入で取り出します。

あとは、表示項目に対応する変数を書き並べることで、統計情報欄を作ります。

import React from "react";
import { useRecoilValue } from "recoil";
import { todoListStatsState } from "./selector";
import { ListGroup } from "react-bootstrap";

export const TodoListStats: React.VFC = () => {
  const {
    totalNum,
    totalCompletedNum,
    totalUncompletedNum,
    percentCompleted
  } = useRecoilValue(todoListStatsState);

  const formattedPercentCompleted = Math.round(percentCompleted);

  return (
    <ListGroup horizontal className="mb-5">
      <ListGroup.Item>すべてのtodo: {totalNum}個</ListGroup.Item>
      <ListGroup.Item>完了したtodo: {totalCompletedNum}個</ListGroup.Item>
      <ListGroup.Item>未完了のtodo: {totalUncompletedNum}個</ListGroup.Item>
      <ListGroup.Item>完了率: {formattedPercentCompleted}%</ListGroup.Item>
    </ListGroup>
  );
};
React ライブラリの recoil を使った todo リストの統計情報欄
todo リストの統計情報欄

TodoItemCreator.tsx ファイルです。 todo を新規作成する役割を務めます。

まずは、 React の useState フックを使って、入力内容を保持するための inputValue と setInputValue を生成します。このコンポーネントでしか使わない state なので、 atom で作る必要はありません。

次に、 Recoil の useSetRecoilState フックを使って、 atom の useRecoilState から『 todoList 本体』を取得して、さらに『 todoList を更新するための setter メソッド』を生成します。 useSetRecoilValue フックとは違い、 useSetRecoilState フックは setter メソッドが生成されることが特徴です。

いったん、 todo に割り振る id の生成するための変数と getId 関数を定義しておきます。 todo をリスト表示で並べる時に ListGroup.Item タグを使うので、このタグの key 属性に id を設定するための措置です。

そして、入力欄の値を取得して、 todo リストに追加する addItem 関数を定義します。 setTodoList メソッドの引数として、配列形式で『スプレッド演算子つきの todo リスト本体』と『 text プロパティに入力値を持つ todo オブジェクト』を渡すことで、 todo リストを更新します。最後に、 setInputValue メソッドに空文字“”を渡して、入力欄を未入力の状態に戻しておきます。

あとは、入力欄とボタンを並べて、新規作成欄を作ります。

import React, { useState } from "react";
import { useRecoilState } from "recoil";
import { todoListState } from "./atoms";
import { FormControl, InputGroup, Button } from "react-bootstrap";

export const TodoItemCreator: React.VFC = () => {
  // useStateフックを使って、使い捨て用のstateとsetStateメソッドを取得
  const [inputValue, setInputValue] = useState("");

  // useSetRecoilStateフックを使って、atomのtodoリストのsetterメソッドを取得
  const [todoList, setTodoList] = useRecoilState(todoListState);

  // todoに割り振るidの生成
  let id = 0;
  const getId = () => {
    return id++;
  };

  // 入力値をtodoリストに追加する関数
  const addItem = (): void => {
    if (inputValue) {
      setTodoList([
        ...todoList,
        { id: getId(), text: inputValue, isComplete: false }
      ]);
      setInputValue("");
    }
  };

  return (
    <div className="mb-5">
      <InputGroup className="mb-3">
        <InputGroup.Text>todo</InputGroup.Text>
        <FormControl
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          aria-label="todo"
        />
      </InputGroup>
      <Button onClick={addItem}>todoを追加する</Button>
    </div>
  );
};
React ライブラリの recoil を使った todo リストの新規作成欄
todo リストの新規作成欄

TodoListFilters.tsx ファイルです。ページに表示する todo の絞り込み条件を選択する役割を務めます。

まずは、 Recoil の useRecoilState フックを使って、 atom の todoListFilterState から『絞り込み条件の文字列』を取得して、さらに『絞り込み条件を更新するための setter メソッド』を生成します。

次に、セレクトボックスの選択値を使って『絞り込み条件の文字列』を更新する updateFilter 関数を定義します。

あとは、絞り込み条件に関するセレクトボックスを設置するだけです。

import React from "react";
import { useRecoilState } from "recoil";
import { todoListFilterState } from "./atoms";

export const TodoListFilters: React.VFC = () => {
    // useSetRecoilStateフックを使って、atomのfilter(表示条件)とsetterメソッドを取得
  const [filter, setFilter] = useRecoilState(todoListFilterState);

  // セレクトボックスの選択値でfilterを更新
  const updateFilter = (value: string): void => {
    setFilter(value);
  };

  return (
    <div>
      <span style={{ marginRight: "10px" }}>フィルター</span>
      <select value={filter} onChange={(e) => updateFilter(e.target.value)}>
        <option value="Show All">すべて</option>
        <option value="Show Completed">完了</option>
        <option value="Show Uncompleted">未完了</option>
      </select>
    </div>
  );
};
React ライブラリの recoil を使った todo リストの絞り込み条件欄
todo リストの表示条件の選択欄 

最後に、 TodoListFilters.tsx ファイルです。 1 つ 1 つの todo を編集・削除機能付きで表示する役割を担います。

TodoItem コンポーネントは、多くの関数を持っているので、コード量が多くなっています。分かりやすく説明するために、キリのいいところでコードを分割して掲載します。ひと通りの説明が終わったら、あらためて TodoListFilters.tsx ファイルの全コードを掲載します。

まずは、ライブラリやファイルから必要なデータを取得する import 一覧です。

import 一覧の直下には、 TodoItem コンポーネントを書きます。コンポーネントの中身は、いったん省略表記です。

import React from "react";
import { Todo } from "./type";
import { useRecoilState } from "recoil";
import { todoListState } from "./atoms";
import { Button } from "react-bootstrap";

export const TodoItem: React.VFC<{ todo: Todo }> = ({ todo }) => {
  // 途中省略 
  return (
    // 途中省略 
  );
};

それでは、 TodoItem コンポーネントの中身を先頭から説明していきます。

まずは、 useRecoilState フックを使って、 atom の todoListState から、『 todo リスト本体』を取得して、さらに setter メソッドを生成します。

次に、この TodoItem コンポーネントが『どのインデックスの todo を描写するのか』を調べるために、 findIndex メソッドを使います。

  // useRecoilStateフックを使って、atomのtodoListStateから『todoリスト本体とsetterメソッドを取得
  const [todoList, setTodoList] = useRecoilState(todoListState);

  // todoListにfindIndexを適用して、propsとして渡ってきたtodoが『todoListのどのインデックスの要素なのか』を探している
  const index = todoList.findIndex((todoItem) => todoItem === todo);

こちらは、『 index に対応する todo を編集・削除した状態の新規 todo リスト』を返却する関数です。この後に説明する『 todo の編集・削除についての関数』で組み込みます。

  // todoリストと『todoの編集内容』を組み合わせて、新規todoリストを作る。
  // 要素の差し替えは、indexを目印に使っている。
  const replaceTodoAtIndex = (arr: Todo[], index: number, newTodo: Todo) => {
    return [...arr.slice(0, index), newTodo, ...arr.slice(index + 1)];
  };

  // todoリストと『todoの編集内容』を組み合わせて、新規todoリストを作る。
  // 要素の除外は、indexを目印に使っている。
  const removeTodoAtIndex = (arr: Todo[], index: number) => {
    return [...arr.slice(0, index), ...arr.slice(index + 1)];
  };

todo に対する変更を todo リストに反映するための 3 つの関数です。

editTodoText 関数は、 todo の入力内容(文字列)を更新する役割を担います。 replaceTodoAtIndex 関数を使って『 todo の入力内容が反映された、新しい todo リスト』を取得します。そして、取得値を setTodoList メソッドに設定することで、 todo リストの状態を更新します。

toggleTodoCompletion 関数は、 todo の完了状態(真偽値)を 更新する役割を担います replaceTodoAtIndex 関数を使って『 todo が完了状態 の真偽値が反映された、新しい todo リスト』を取得します。そして、取得値を setTodoList メソッドに設定することで、 todo リストの状態を更新します。

editTodoText 関数は、 todo を削除する役割を担います。 removeTodoAtIndex 関数を使って『インデックスに合致しない todo のみを残した、新しい todo リスト』を取得します。そして、取得値を setTodoList メソッドに設定することで、 todo リストの状態を更新します。

  // todoの編集内容を反映してtodoリストを更新する関数
  const editTodoText = (value: string): void => {
    const newList = replaceTodoAtIndex(todoList, index, {
      ...todo,
      text: value
    });

    setTodoList(newList);
  };

  // todoが完了したかどうかを更新する関数
  const toggleTodoCompletion = (): void => {
    const newList = replaceTodoAtIndex(todoList, index, {
      ...todo,
      isComplete: !todo.isComplete
    });

    setTodoList(newList);
  };

  // todoを除外して、todoリストを更新する関数
  const deleteItem = () => {
    const newList = removeTodoAtIndex(todoList, index);

    setTodoList(newList);
  };

最後に、以上の関数を組み込んだ入力欄とボタンを設置します。

  return (
    <div
      style={{
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center"
      }}
    >
      <input
        type="text"
        value={todo.text}
        onChange={(e) => editTodoText(e.target.value)}
      />
      <div>
        <input
          style={{ marginRight: "5px" }}
          type="checkbox"
          checked={todo.isComplete}
          onChange={toggleTodoCompletion}
        />
        完了
      </div>

      <Button variant="danger" onClick={deleteItem}>
        削除
      </Button>
    </div>
  );

TodoItem.tsx ファイルの全てのコードは、以下のとおりです。

import React from "react";
import { Todo } from "./type";
import { useRecoilState } from "recoil";
import { todoListState } from "./atoms";
import { Button } from "react-bootstrap";

export const TodoItem: React.VFC<{ todo: Todo }> = ({ todo }) => {
  // useRecoilStateフックを使って、atomのtodoListStateからtodoリスト本体とsetterメソッドを取得
  const [todoList, setTodoList] = useRecoilState(todoListState);
  // todoListにfindIndexを適用して、propsとして渡ってきたtodoが『todoListのどのインデックスの要素なのか』を探している
  const index = todoList.findIndex((todoItem) => todoItem === todo);

  // todoリストと『todoの編集内容』を組み合わせて新規配列を作る。
  // 要素の差し替えは、indexを目印に使っている。
  const replaceTodoAtIndex = (arr: Todo[], index: number, newTodo: Todo) => {
    return [...arr.slice(0, index), newTodo, ...arr.slice(index + 1)];
  };

  // todoリストから『todoの編集内容』を組み合わせて新規配列を作る。
  // 要素の除外は、indexを目印に使っている。
  const removeTodoAtIndex = (arr: Todo[], index: number) => {
    return [...arr.slice(0, index), ...arr.slice(index + 1)];
  };

  // todoの編集内容を反映してtodoリストを更新する関数
  const editTodoText = (value: string): void => {
    const newList = replaceTodoAtIndex(todoList, index, {
      ...todo,
      text: value
    });

    setTodoList(newList);
  };

  // todoが完了したかどうかを更新する関数
  const toggleTodoCompletion = (): void => {
    const newList = replaceTodoAtIndex(todoList, index, {
      ...todo,
      isComplete: !todo.isComplete
    });

    setTodoList(newList);
  };

  // todoを除外して、todoリストを更新する関数
  const deleteItem = () => {
    const newList = removeTodoAtIndex(todoList, index);

    setTodoList(newList);
  };

  return (
    <div
      style={{
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center"
      }}
    >
      <input
        type="text"
        value={todo.text}
        onChange={(e) => editTodoText(e.target.value)}
      />
      <div>
        <input
          style={{ marginRight: "5px" }}
          type="checkbox"
          checked={todo.isComplete}
          onChange={toggleTodoCompletion}
        />
        完了
      </div>

      <Button variant="danger" onClick={deleteItem}>
        削除
      </Button>
    </div>
  );
};

以上のファイルによって、このような todo リストが出来上がります。


この記事を知り合いに共有する
URLをコピーする
URLをコピーしました!
目次
閉じる