🧲

recoilに入門してみる

2022/11/17に公開約6,400字

recoilとは?

Reduxに変わる状態管理ライブラリ。試作品らしいのですが、新規の開発では使われているそうです。
使ってみた感想ですが、Reduxより理解しやすかったですね。
atomという関数、Objectと思ってましたが、これを作って、contextを渡すだけという単純なものですね。
ReduxTookitみたいに、actionsとか、dispatchとか、書かなくていいから分かりやすい💡
公式ページ
https://recoiljs.org/

公式ページを翻訳してみた

概要

Recoilでは、アトム(共有状態)からセレクタ(純粋関数)を経て、Reactコンポーネントへと流れるデータフロー・グラフを作成することができます。アトムは、コンポーネントがサブスクライブすることができる状態の単位です。セレクタは、この状態を同期または非同期で変換します。

Atoms(原子という意味)

アトムは状態の単位です。アトムが更新されると、購読している各コンポーネントが新しい値で再レンダリングされます。アトムは、実行時に作成することもできます。アトムは、Reactのローカルコンポーネントの状態の代わりに使用することができます。複数のコンポーネントから同じアトムを使用する場合、すべてのコンポーネントがその状態を共有します。

アトムはatom関数を用いて作成する。

const fontSizeState = atom({
  key: 'fontSizeState',
  default: 14,
});

アトムには一意なキーが必要で、これはデバッグや永続化、そしてすべてのアトムのマップを見ることができる特定の高度なAPIで使用されます。2つのアトムが同じキーを持つことはエラーになるので、グローバルに一意であることを確認してください。Reactコンポーネントのステートと同様に、アトムはデフォルト値を持ちます。

コンポーネントからアトムを読み書きするには、useRecoilStateというフックを使用します。これはReactのuseStateと同じですが、コンポーネント間で状態を共有することができるようになりました。

function FontButton() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  return (
    <button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}>
      Click to Enlarge
    </button>
  );
}

ボタンをクリックすると、ボタンのフォントサイズが1つ大きくなります。しかし、今度は他のコンポーネントも同じフォントサイズを使用できるようになりました。

function Text() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  return <p style={{fontSize}}>This text will increase in size too.</p>;
}

セレクタ

セレクタは純粋な関数であり、アトムや他のセレクタを入力として受け取ります。これらの上流のアトムやセレクタが更新されると、セレクタの関数が再評価されます。コンポーネントは、アトムと同様にセレクタにサブスクライブすることができ、セレクタが変更されると再レンダリングされます。

セレクタは、ステートに基づいて派生データを計算するために使用されます。これにより、最小限の状態がアトムに保存され、他のすべてがその最小限の状態の関数として効率的に計算されるため、冗長な状態を回避することができます。セレクタは、どのコンポーネントがセレクタを必要とし、どの状態に依存しているかを追跡するため、この関数的なアプローチを非常に効率的にする。

コンポーネントから見ると、セレクタとアトムは同じインターフェースを持っているので、互いに置き換えることができる。

セレクタはセレクタ関数を用いて定義します。

const fontSizeLabelState = selector({
  key: 'fontSizeLabelState',
  get: ({get}) => {
    const fontSize = get(fontSizeState);
    const unit = 'px';

    return `${fontSize}${unit}`;
  },
});

getプロパティは、計算される関数です。この関数は、渡されたget引数を使用して、アトムや他のセレクタの値にアクセスすることができます。他のアトムやセレクタにアクセスするときは常に、他のアトムやセレクタを更新するとこのアトムが再計算されるような依存関係が作成されます。

この fontSizeLabelState の例では、セレクタは fontSizeState アトムという依存関係を 1 つ持っています。概念的には、fontSizeLabelState セレクタは、入力として fontSizeState を受け取り、出力としてフォーマットされたフォントサイズのラベルを返す、純粋な関数のように振る舞います。

セレクタは、useRecoilValue()を使って読み取ることができます。これは、アトムまたはセレクタを引数として受け取り、対応する値を返します。fontSizeLabelStateセレクタは書き込みができないので、useRecoilState()は使いません(書き込み可能なセレクタについては、セレクタAPIリファレンスを参照してください)。

function FontButton() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  const fontSizeLabel = useRecoilValue(fontSizeLabelState);

  return (
    <>
      <div>Current font size: {fontSizeLabel}</div>

      <button onClick={() => setFontSize(fontSize + 1)} style={{fontSize}}>
        Click to Enlarge
      </button>
    </>
  );
}

ボタンをクリックすると、ボタンのフォントサイズが大きくなり、同時にフォントサイズラベルも現在のフォントサイズを反映したものに更新されるようになりました。


チュートリアルをTypeScriptでやってみた

最初は公式ドキュメントを読んでやっていたのだが、うまくいかなかった?
でどうしたかというと、動くサンプルで勉強してRecoilRootの配置場所とatomってどんな仕組みで動いているのか調べてみた。

フォルダ構成はこんな感じですね。componentsにカウンターのコンポーネントを配置、utilsにatomを配置しました。
App.tsxでRecoilRootを配置して他のページにも値を渡せるようになっています。これでPropsがいらなくなります。

.
├── App.tsx
├── components
│   └── RecoilComponent.tsx
├── main.tsx
├── utils
│   └── atom.ts
└── vite-env.d.ts

まずは、Recoilを使うためのRecoilRootを設定します。これを設定するだけで準備OK

App.tsx
import { RecoilRoot } from 'recoil'
import { RecoilComponent } from './components/RecoilComponent'


function App() {

  return (
    <div className="App">
      {/* RecoilRootでWrapすると値をどこからでも渡せるようになる! */}
      <RecoilRoot>
        <RecoilComponent />
      </RecoilRoot>
    </div>
  )
}
export default App

こちらに、ユニークなkyeとdefalt値(数値や空の文字を用意する)を用意する

utils/atom.ts
import { atom } from "recoil";
// keyには、他と名前が被らないようにする
// 同じcountStateだったら、tsx内の値が全て変わる!
export const recoilAtom = atom({
  key: "counterState",
  default: 0,
});

export const textState = atom({
  key: 'textState', // unique ID (with respect to other atoms/selectors)
  default: '', // default value (aka initial value)
});

atomで値を渡すコンポーネントを作成する。これだけ書くだけでカウンターの機能なら簡単に作れる。
Reduxよりわかりやすい。
公式のフォームの機能も追加。TypeScriptで書くとonChangeのところで型定義が必要でした。
分からないときは、any型で書いてたのですが、これはよくない!
Eventの方があるようで、こちらをまとめてつくって使っても良いみたいですし、今回はonFocusという方でも行けました。

いっぱい書かれてますね

// 引数のe:anyを防止する方法、onFocusだけでできそう?
// typeをe:Propsと使っても良い?
type Props = {
  onClick: (event: React.MouseEvent<HTMLInputElement>) => void;
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
  onkeypress: (event: React.KeyboardEvent<HTMLInputElement>) => void;
  onBlur: (event: React.FocusEvent<HTMLInputElement>) => void;
  onFocus: (event: React.FocusEvent<HTMLInputElement>) => void;
  onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
  onClickDiv: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
};

こちらがコンポーネント。
カウンターと入力フォームだけですが最初はこれぐらいがわかりやすいですね。

components/RecoilComponent.tsx
import React from "react";
import { useRecoilState } from "recoil";
import { recoilAtom, textState } from "../utils/atom";
// 引数のe:anyを防止する方法、onFocusだけでできそう?
// typeをe:Propsと使っても良い?
type Props = {
  onClick: (event: React.MouseEvent<HTMLInputElement>) => void;
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
  onkeypress: (event: React.KeyboardEvent<HTMLInputElement>) => void;
  onBlur: (event: React.FocusEvent<HTMLInputElement>) => void;
  onFocus: (event: React.FocusEvent<HTMLInputElement>) => void;
  onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
  onClickDiv: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
};

export const RecoilComponent = () => {
  const [counter, setCounter] = useRecoilState(recoilAtom);

  const [text, setText] = useRecoilState(textState);

  const onChange = (event: React.FocusEvent<HTMLInputElement>) => {
    console.log("React.FocusEvent<HTMLInputElement>を実行");
    setText(event.target.value);
  };

  return (
    <div>
      <p>{counter}</p>
      <button onClick={() => setCounter((counter) => counter + 1)}>++     </button>
      <br />
      <input type="text" value={text} onChange={onChange} />
      <br />
      Context: {text}
    </div>
  );
};

こんな感じのものを作れました!

最後に

以前ReduxTookitを学習していたのですが、全然わかりませんでした。
でもRecoilならわかりやすくて、導入しやすいかも。
まずは動くものを作って理解を始めることが大事ですね。

Discussion

ログインするとコメントできます