1
0

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.

Recoil の Atom Effects を使ってViewの状態管理からIOに関する処理を剥がしてみる

Posted at

Reactで軽量な状態管理ライブラリを調べるとRecoilが有力そうなので試してみた。
その際に、LocalStorageや非同期処理でDBに保存する処理などを Atom Effects を利用してView側のロジックから引き剥がせたので、そのコードを簡略化したものを記載しておく。
ほとんど公式と同じです。

前提条件

  • React: 18.2.0
  • Recoil: 0.7.7

ローカルストレージに永続化させるサンプル実装

  • ユーザーは画面で2つの状態のどちらかを選択できる
  • 次回以降も反映させるためローカルストレージを用いて状態を永続化する(初回読み込み時に反映)

このような仕様を満たすために、Recoilを用いると次のような実装ができた。

  • state.ts // 状態管理用
state.ts
import { atom } from "recoil";

const localStorageKey = "SAMPLE_KEY"

type SampleType = "A" | "B";

type SampleStateProps = {
  hoge: SampleType;
};

export const SampleState = atom<SampleStateProps>({
  key: "sample",
  default: {
    hoge: "A",
  },
  effects: [
    ({ onSet, setSelf, trigger }) => {
      if (trigger === "get") {
        // 初回読み込み時に実行される

        // ローカルストレージから取得し、値が存在すれば state に反映
        const savedValue = localStorage.getItem(localStorageKey);

        if (savedValue != null) {
          setSelf(JSON.parse(savedValue));
        }
      }

      // state が更新されたら実行される
      onSet((newValue) => {
        // ローカルストレージに保存
        localStorage.setItem(localStorageKey, JSON.stringify(newValue));
      });
    },
  ],
});

effects は配列を受け取るようになっており、用途に応じて複数渡すことで、状態が更新された際の副作用の処理などを複数登録しておける。
今回は初回読み込み時の処理と、状態が更新されたらローカルストレージにも反映させるようにした。

  • app.tsx // 表示用
app.tsx
import { useRecoilState } from "recoil";
import { SampleState } from "./state";

const App = () => {
  const [state, setState] = useRecoilState(SampleState);
  const turnToA = () => setState({ hoge: "A" });
  const turnToB = () => setState({ hoge: "B" });

  return (
    <div>
      <p>{state.hoge}</p>
      <div>
        <button onClick={turnToA}>turn to A</button>
      </div>
      <div>
        <button onClick={turnToB}>turn to B</button>
      </div>
    </div>
  );
};

export default App;

動かしてみると確かにローカルストレージに永続化し、次回読み込み時も反映された。
View側からは単に状態更新を行っているだけに見えるので、余計なロジックを気にしなくても良くなった。

Tips

Effects に配置する処理は AtomEffect を用いて別途関数として定義しておけるので、複数登録する場合は役割に応じて分割しておくと保守しやすいと感じた。

state.ts
import { AtomEffect, atom } from "recoil";

...省略

const localStorageEffect: (key: string) => AtomEffect<SampleStateProps> = (key: string) => ({setSelf, onSet, trigger}) => {
  const getByLocalStorage = async () => {
    // ローカルストレージから取得し、値が存在すれば state に反映
    const savedValue = await localStorage.getItem(key);

    if (savedValue != null) {
      setSelf(JSON.parse(savedValue));
    }
  };
  if (trigger === "get") {
    // 初回読み込み時に実行される
    getByLocalStorage();
  }

  // state が更新されたら実行される
  onSet((newValue) => {
    // ローカルストレージに保存
    localStorage.setItem(key, JSON.stringify(newValue));
  });
};

export const SampleState = atom<SampleStateProps>({
  key: "sample",
  default: {
    hoge: "A",
  },
  effects: [
    localStorageEffect(localStorageKey),
    // ロギングなどを別途追加する...
  ],
});

DBに保存する場合の処理は割愛させていただくが、基本的には onSet の状態更新のイベントを受けて副作用的に更新する。
ただ今回のローカルストレージとは異なり、保存に失敗するケースが発生するので注意が必要になる。

場合にもよるが、DB保存専用の状態を設けて "保存中" | "成功" | "失敗" のような状態で管理しつつ、ユーザーフレンドリーな表示をしつつ、リカバリー可能な処理を設けるのが良いだろう。

見てくださりありがとうございました。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?