17
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【React】Recoilを使って簡単なTODOアプリを作り、状態管理を理解する

Last updated at Posted at 2023-07-01

概要-Recoilとは

Reactにおける状態管理ライブラリの一つです。

状態管理とは

本来のReactの状態管理では、親のコンポーネントから子のコンポーネントへpropsという形で情報を渡しますが、複雑なアプリケーションになるとバケツリレーのような形になってしまい、管理が大変になってしまいます。

状態管理のライブラリを使用することで、Store(ストア)に保存しておけば、直接コンポーネントから参照することができます。

どのような時に使われるか

例えば

  • ユーザーがログインしているかしていないか
  • 投稿内容のタイトルや内容

RecoilとReduxの違い

Redux 大きく一つのStoreがある
Recoil 複数のStoreがある

Recoilの一つ一つのストアは 「atom(アトム)」 と呼ばれる最小単位です。
複数のatomで状態を管理し、必要なコンポーネントで呼び出して使用することができます。

Reduxの一つのStoreで管理するデメリット・Recoilのメリット

Reduxでは1つのStoreで管理をしているため、もしStoreに空のオブジェクトが更新された時、Reduxを使用しているすべてのコンポーネントが空のオブジェクトに上書きされてしまいます。上手く表示されないバグの原因を生み出す可能性があります。

一方でRecoilでは複数の最小単位のバスターを使っているため、全体を更新されるReduxのデメリットを抑えることができます。

また、atom自体に保管されている値を更新するためには 「Selecter(セレクター)」 という関数を使用します。

RecoilとuseContextの違い

useContextはReactの状態管理のhooksです。
useContextは、根本となるコンポーネントで 「Provider(プロバイダー)」 を宣言し、コンポーネント配下のコンポーネントでもグローバルな状態変数を扱えるようになります。

useContextだと一番下の枝の部分を更新したい時に、一番根本となるコンポーネントを更新しないといけません。
useContextのデメリットは、祖先のコンポーネントも更新する必要があるため、中間にあるコンポーネントも全部レンダリングされてしまい、不要な再レンダリングが発生してしまうことです。

Recoilを使用することで、該当のコンポーネントだけに影響するため、useContextのように祖先から更新される心配はありません。

ですが、Recoilは複数のatomを使用することから複数人で開発する際には、意図しない状態変更が行われる可能性もあります。

「複数人でRecoilを使うときに意図しない状態変更が行われる可能性」
色々なコンポーネントから利用され、状態を更新されるため、Aさんが担当したコンポーネントで更新した状態はCさんが担当したコンポーネントでは意図した状態でない可能性があります。

パフォーマンスの観点や、運用のしやすさからどのライブラリを使用するかの選定をする必要があります。

RecoilでTODOアプリを作る

ReactとTypecript、Recoilを使って簡単なTODOアプリを作ります。

ezgif.com-video-to-gif.gif

開発環境設定、Recoilのインストール

今回はviteを使用します。

$ npm create vite

Project nameを設定し、React・Typescriptを選択し、ディレクトリを作成します。

ディレクトリを開いて、下記を実行し、インストールします。

$ npm i

ローカルサーバーが5173番で開きます。

$ npm run dev

Recoilをインストールします。

$ npm i recoil

Typescriptも使用できるようにインストールしておきます。

$ npm i @types/recoil

RecoilRootの設定

Recoilを使用するためにコンポーネントをRecoilRootで囲みます。

src/App.tsx
import "./App.css";
import AddTask from "./components/AddTask";
import InputTask from "./components/InputTask";
import { RecoilRoot } from "recoil";

function App() {
  return (
    <RecoilRoot>
      <div className="App">
        <InputTask />
        <AddTask />
      </div>
    </RecoilRoot>
  );
}

export default App;

コンポーネントの作成

src/componentsフォルダを追加し、今回のアプリに必要なInputTask.tsxAddTask.tsxを作成します。

useRecoilState()

stateの取得・更新を行う場合、useRecoilState()を使用します。
useRecoilState()の引数には使用したいstateを渡します。

useRecoilValue()

stateの値だけを取得する場合、useRecoilValue()を使用します。
useRecoilValue()は引数に使用するstateのkeyを渡します。

src/components/InputTask.tsx
import React, { useCallback } from "react";
import "./InputTask.css";
import { inputTitleState } from "../states/inputTitleState";
import { useRecoilState } from "recoil";
import { addTitleState } from "../states/addTitleState";

// idの値をランダムに設定するための関数
const getKey = () => Math.random().toString(32).substring(2);

const InputTask = () => {
  const [inputTitle, setInputTitle] = useRecoilState(inputTitleState);
  const [addTitle, setAddTitle] = useRecoilState(addTitleState);

  const onChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      setInputTitle(e.target.value);
    },
    [inputTitle]
  );

  const handleClick = () => {
    setAddTitle([...addTitle, { id: getKey(), title: inputTitle }]);
    setInputTitle("");
  };

  return (
    <div className="inputField">
      <input type="text" className="inputTitle" onChange={onChange} value={inputTitle}/>
      <button type="button" className="addButton" onClick={handleClick}>
        追加
      </button>
    </div>
  );
};

export default InputTask;
src/components/AddTask.tsx
import React from "react";
import { useRecoilValue } from "recoil";
import { addTitleState, addTitleStateLength } from "../states/addTitleState";

const AddTask = () => {
  // useRecoilValue:値だけを取ってくる
  const addTitle = useRecoilValue(addTitleState);
  const addTitleLength = useRecoilValue(addTitleStateLength);
  return (
    <div className="taskField">
      <div>{addTitleLength}個のタスク</div>
      <ul>
        {addTitle.map((task) => (
          <li key={task.id}>{task.title}</li>
        ))}
      </ul>
    </div>
  );
};

export default AddTask;

Atomの設定

AtomはStateを管理するための最小単位です。
今回はsrc/statesフォルダを作成し、その中にAtomの設定を行います。

Atomは状態の一部を表します。Atomは、任意のコンポーネントから読み書きすることができます。Atomの値を読み取るコンポーネントは暗黙的にそのAtomにサブスクライブされるため、Atomが更新されると、そのアトムにサブスクライブされているすべてのコンポーネントが再レンダリングされます。

Recoil公式ドキュメントより引用

src/states/inputTitleState.ts
import { atom } from "recoil";

export const inputTitleState = atom<string>({
  key: "inputTitleState",
  default: "",
});

Selecter

atomの中になんらかの操作を行いたい(非同期処理・配列の長さの指定など)ときはSelecterを使います。

セレクターは、派生状態の一部を表します

Recoil公式ドキュメントより引用

今回はaddTitleStateの配列の長さを取得するためにSelecterを使用しました。

src/states/addTitleState.ts
import { atom, selector } from "recoil";
import { Task } from "../types/Task";

export const addTitleState = atom<Array<Task>>({
  key: "addTitleState",
  default: [],
});

// atomの中になんらかの操作を行いたい(非同期処理・配列の長さの指定など)ときはSelecterを使う
export const addTitleStateLength = selector<number>({
  key: "addTitleStateLength",
  // 長さを取得
  get: ({ get }) => {
    const addTitleNumber: Array<Task> = get(addTitleState);
    return addTitleNumber?.length || 0;
  },
});

TypeScriptの型ファイルの設定

src/types/Task.ts
export type Task = {
  id: string;
  title:string;
}

まとめ

Reactの状態管理はさまざまなライブラリや手法が存在しているため、設計時の選定が難しくなっています。
Google Trends で比較をしてみると、Recoilが徐々に伸びてきている傾向があります。

スクリーンショット 2023-07-01 19.36.04.png

状態管理のキャッチアップはしっかり行っていく必要があることを学びました。

参考

Recoil公式ドキュメント

17
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?