🌀

Vite+React+TypeScript+EsLintに Reciolを導入して Stateをローカルストレージで永続化する

2022/02/23に公開

今回はVite+React+TypeScript+EsLintにRecoilを導入して、RecoilのStateをローカルストレージで永続化してみるまでの手順です。

Recoil導入

特に難しいことはなく、Recoil公式の通りです。
https://recoiljs.org/docs/introduction/installation

Recolインストール

Recoilインストールです。

npm install recoil

eslintrc.ymlの設定

「eslint-plugin-react-hooks」プラグインを導入している場合は、Recoil用の設定を追加することが推奨されています。

「eslint-plugin-react-hooks」プラグインはEsLintの初期化コマンド「npx eslint --init」でインストールされています。
設定がまだであれば追加しておいた方がよいでしょう。

例えば、「eslint-plugin-react-hooks」の設定を以下のようにしている場合、

.eslintrc.yml(抜粋)
plugins:
  - react
+  - react-hooks 
  - '@typescript-eslint'
  - jest
  - jest-dom
  - testing-library
  
rules:
  # Reactのインポートをチェックしない
  react/react-in-jsx-scope: off
  # セミコロンつけない
  semi:
    - error
    - never
  # デフォルトエクスポートをエラーにする
  import/prefer-default-export: off
  import/no-default-export: error
+  # フックのルールに準拠するようにコードを強制する
+  react-hooks/rules-of-hooks: error
+  # エフェクトの依存配列のチェック
+  react-hooks/exhaustive-deps: warn

RecoilのuseRecoilCallbackの設定を追加することが推奨されています。
useRecoilCallback() に渡された依存関係が正しく指定されていない場合は警告されます。

.eslintrc.yml(抜粋)
rules:
  # Reactのインポートをチェックしない
  react/react-in-jsx-scope: off
  # セミコロンつけない
  semi:
    - error
    - never
  # デフォルトエクスポートをエラーにする
  import/prefer-default-export: off
  import/no-default-export: error
  # フックのルールに準拠するようにコードを強制する
  react-hooks/rules-of-hooks: error
  # エフェクトの依存配列のチェック
-  react-hooks/exhaustive-deps: warn
+  react-hooks/exhaustive-deps: 
+    - warn
+    - additionalHooks: '(useRecoilCallback|useRecoilTransaction_UNSTABLE)'

動作確認用のプログラム

動作確認用に簡単なプログラムを作成します。
テキストボックスに入力した値を、ボタンクリックで登録します。

Recoilを使用するコンポーネントは、親ツリーのどこかに<RecoilRoot>を配置する必要があります。ここではAppコンポーネントに配置しています。

App.tsx
import { RecoilRoot } from 'recoil'
import { AtomSample } from './AtomSample'

export function App() {
  return (
    <RecoilRoot>
      <AtomSample />
    </RecoilRoot>
  )
}

「登録した値」を管理するためのAtomを作成します。

atoms.ts
import { atom } from 'recoil'

export const addedValue = atom({
  key: 'addedValue',
  default: '',
})

登録ボタンのクリックで、先ほど作成した「登録した値」Atom にテキストボックスに入力した値をセットします。

AtomSample.tsx
import React from 'react'
import { useRecoilState } from 'recoil'
import * as atoms from './atoms'

export function AtomSample() {
  const [inputValue, setInputValue] = React.useState('')
  const [addedValue, setAddedValue] = useRecoilState(atoms.addedValue)

  const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setInputValue(event.target.value)
  }

  const onClick =() => {
    setAddedValue(inputValue)
    setInputValue('')
  }

  return (
    <div>
      <input type="text" value={inputValue} onChange={onChange} />
      <button type="button" onClick={onClick}>登録</button>
      <p>登録値:{addedValue}</p>
    </div>
  )
}

Atomの値をローカルストレージに保存する

先ほど作成したプログラムですが、リロードすると登録したAtomの値が消えてしまいます。

そこで、Atomの値をローカルストレージに保存します。

方法はいくつかあるようです。

今回は公式サイトの AtomEffect を使用する方法で、実装します。
コチラのサイトにtypescript用がありましたので、そのままいただきました。
参考:https://github.com/facebookexperimental/Recoil/issues/6

atom.ts
import { atom, AtomEffect, DefaultValue } from 'recoil'

+ const localStorageEffect: <T>(key: string) => AtomEffect<T> =
+  (key: string) =>
+  ({ setSelf, onSet }) => {
+    const savedValue = localStorage.getItem(key)
+ 
+    if (savedValue != null) {
+      setSelf(JSON.parse(savedValue))
+    }
+ 
+    onSet((newValue) => {
+      if (newValue instanceof DefaultValue) {
+        localStorage.removeItem(key)
+      } else {
+        localStorage.setItem(key, JSON.stringify(newValue))
+      }
+    })
+  }



export const addedValue = atom({
  key: 'addvalue',
  default: '',
+  effects_UNSTABLE: [
+    localStorageEffect<string>('addedValue')
+  ]
})

Atom Effects は Atom に値がセットされたときに、別の処理を実行させることができます。
setSelf には Atom の値を設定またはリセットするためのコールバック関数を指定します。
onSet は Atom の値が変化したときのコールバック関数を指定します。

リロードしても値が保持されるようになりました。

AtomEffect には、他にも
Atom の値がセットされたら、ログを記録したり、変更履歴を記録したりもできるようです。

Discussion