かべブログ

TypeScript, React, Goなどでまったり個人開発。たまに競プロ。

Recoil 素振り

はじめに

こんにちは、かべです。今日は話題の(?)状態管理ライブラリ Recoil を使って遊んでみようと思います。たぶん流行りには乗り遅れてますが、その辺は気にしない。

公式の todo アプリを作るチュートリアルがあるのですが、今回はもう少し簡単にさくっとアプリを作っていこうと思います。使う機能はほとんど変わりません。実装したい機能としては、

  • 好きな数字を選んでリストに追加し、表示する
  • 表示する数字を偶数か奇数かでフィルタリング出来る

完成イメージはこのようになります。

f:id:okb_okb:20210409172231p:plain
数字を追加できる

f:id:okb_okb:20210409172257p:plain
数字のフィルタリングができる

では、早速作って行きましょう。コードはこちらに置いてあります。

環境

今回の開発は以下の環境で行いました。

  • Recoil 0.2.0
  • Ubuntu 20.04.1
  • yarn 1.22.5
  • React 17.0.2
  • TypeScript 4.2.4

React アプリの作成、Recoil の導入

まずは npx create-react-app [好きな名前] --template typescript を叩いて React アプリを作成します。完成したら、yarn add recoil で Recoil を入れたり、使わない /public の画像等整理してあげたりしましょう。

まずは Recoil で状態管理が出来るように設定を行います。状態管理をしたい部分だけ<RecoilRoot></RecoilRoot> で囲めばよいのですが、今回は規模が小さいので雑に src/index.tsx に書きます。

src/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")
);

atom, selector の作成

Recoil では atom と呼ばれるもので状態を持ち、selector と呼ばれるものでそれを分類したり変形した値を返したりすることが出来ます。

今回必要になる atom や selector の内容としては、次のようになります。

atom

  • 追加された数字を持つ配列
  • どのような数字を表示するか(偶数、奇数、全て)

selector

  • 数字の配列から指定された条件(偶数、奇数、全て)を満たす数字のみからなる配列

これらを src/recoil/ というディレクトリの中に作っていきます。atom はアプリ全体で一意の key とデフォルトの値を持ち、次のように書くことが出来ます。

src/recoil/atoms.ts

import { atom } from "recoil";

export const numbersState = atom({
  key: "numbersState",
  default: [] as number[],
});

export const numbersFilterState = atom({
  key: "numbersFilterState",
  // "all", "odd", "even" が入る
  default: "all",
});

selector はアプリ全体で一意の key と、他の atom や selector の値から求められる値を返す get などからなります。他にも set などのオプションもありますが、今回は使いません。今回使用する selector は次のように書くことが出来ます。

src/recoil/selectors.ts

import { selector } from "recoil";
import { numbersState, numbersFilterState } from "./atoms";

export const filteredNumbersState = selector({
  key: "filteredNumbersState",
  get: ({ get }) => {
    const filter = get(numbersFilterState);
    const numbers = get(numbersState);

    switch (filter) {
      case "odd":
        return numbers.filter((number) => number % 2 !== 0);
      case "even":
        return numbers.filter((number) => number % 2 === 0);
      default:
        return numbers;
    }
  },
});

atoms から取ってきた値を filter と numbers に入れ、switch 文で返り値を分岐させています。

コンポーネント内での使用

atom と selector が整ったので、実際にアプリ内で使用していきます。まずは新しい数字を追加するためのコンポーネントです。

src/components/AddNumbers.tsx

import { FC, useState } from "react";
import { useSetRecoilState } from "recoil";
import { numbersState } from "../recoil/atoms";

const AddNumbers: FC = () => {
  const [inputNumber, setInputNumber] = useState(0);
  const setNumbers = useSetRecoilState(numbersState);

  const increment = () => {
    setInputNumber((num) => num + 1);
  };
  const decrement = () => {
    setInputNumber((num) => num - 1);
  };

  // atom 内の value を変更
  const addNumber = () => {
    setNumbers((oldNumbers) => {
      return [...oldNumbers, inputNumber];
    });
    setInputNumber(0);
  };

  return (
    <>
      <button onClick={decrement}>-1</button> {inputNumber}{" "}
      <button onClick={increment}>+1</button>{" "}
      <button onClick={addNumber}>追加</button>
    </>
  );
};

export default AddNumbers;

useSetRecoilState を使うと atom などの値を書き換えることが出来ます。書き換えたい atom と前の値を受け取って新しい値を返す関数を与えると、書き換えが行われます。

次にどのような数字を表示するかユーザーが決められるセレクトボックスを作ります。

src/components/FilterNumbers.tsx

import { FC } from "react";
import { useRecoilState } from "recoil";

import { numbersFilterState } from "../recoil/atoms";

const FilterNumbers: FC = () => {
  const [filter, setFilter] = useRecoilState(numbersFilterState);

  // atom 内の value を変更
  const updateFilter = (event: React.ChangeEvent<{ value: unknown }>) => {
    setFilter(event.target.value as string);
  };

  return (
    <>
      <select value={filter} onChange={updateFilter}>
        <option value="all">全て</option>
        <option value="odd">奇数</option>
        <option value="even">偶数</option>
      </select>
    </>
  );
};

export default FilterNumbers;

useRecoilState を使うと atom の読み書きが出来ます。

最後に数字を表示するコンポーネントを作ります。

src/components/ShowNumbers.tsx

import { FC } from "react";
import { useRecoilState } from "recoil";

import { numbersFilterState } from "../recoil/atoms";

const FilterNumbers: FC = () => {
  const [filter, setFilter] = useRecoilState(numbersFilterState);

  // atom 内の value を変更
  const updateFilter = (event: React.ChangeEvent<{ value: unknown }>) => {
    setFilter(event.target.value as string);
  };

  return (
    <>
      <select value={filter} onChange={updateFilter}>
        <option value="all">全て</option>
        <option value="odd">奇数</option>
        <option value="even">偶数</option>
      </select>
    </>
  );
};

export default FilterNumbers;

値だけ欲しい時は useRecoilValue を使います。あとはコンポーネントを並べれば完成です!

src/App.tsx

import { FC } from "react";
import AddNumbers from "./components/AddNumbers";
import FilterNumbers from "./components/FilterNumbers";
import ShowNumbers from "./components/ShowNumbers";

const App: FC = () => {
  return (
    <>
      <AddNumbers /> <FilterNumbers />
      <ShowNumbers />
    </>
  );
};

export default App;

yarn start で実行し、localhost:3000 を確認してみましょう。

終わりに

体感 Redux よりかなり楽に状態管理が出来た気がします。今(2021/4/9) 0.2.0 なので、これから機能の拡充等楽しみです。

参考

Recoil