Reactにおける最新のグローバルステート管理ライブラリであるRecoilを試してみたいと思います。グローバルで利用できるuseStateと捉えるのが一番イメージに近いかもしれませんね。手軽に使えるのでマスターしておきたいところです。
redux版も書きました。こちらをどうぞ。
やりたいこと
- recoilのテスト
- グローバルで値が維持されていることを確認するために最低限のrouting(ページ移動)も行う
- 値の永続化も行う
やりたいことイメージをまとめると下記のような感じ。
準備
作業場の確保とライブラリのインストール
npx craete-react-app recoil-test
cd recoil-test
npm install recoil react-router-dom
recoilと関係のない実装(骨組みづくり)
まず、複数ページからなる最低限の構造を作ります。とりあえずHomeとAboutからなるサイトにしてみます。
src以下にHome.jsとAbout.jsを作ります。
touch src/Home.js src/About
Home.jsを記述。
import { Link } from "react-router-dom";
const Home = () => {
return (
<>
<h1>Home</h1>
<Link to="/about">About</Link>
</>
);
}
export default Home;
移動には<a></a>ではなく<Link></Lin>を利用してください。じゃないと、ページが更新されrecoilによる値の維持ができません。
About.jsを記述。
import { Link } from "react-router-dom";
const About = () => {
return (
<>
<h1>About</h1>
<Link to="/">Home</Link>
</>
);
}
export default About;
作成したHome.jsとAbout.jsを読み込んでサイトの構造を作ります。
import { BrowserRouter, Switch, Route } from "react-router-dom";
import Home from "./Home";
import About from "./About";
const App = () => {
return (
<BrowserRouter>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" exact component={About} />
<Route render={() => <p>Page not found.</p>} />
</Switch>
</BrowserRouter>
);
}
export default App;
一度、動作を確認して置きましょう。
npm start
ページ間を移動する簡単なサイトができました。
Recoilを利用して、このページ間で状態を維持するようにしてみたいと思います。
Recoilを利用した実装
ここからが本題です。
RecoilRootで状態を共有したエリアを囲む
ReduxでもContextでも値を共有したいエリアを専用タグで囲みました。recoilではRecoilRootです。
多くの場合、サイト全体で値を共有するのでindex.jsに記述します。
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { RecoilRoot } from "recoil"; //追加
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>,
document.getElementById('root')
);
reportWebVitals();
Atomを作成
reacoilではatomと呼ばれる共有の変数を作成し値を共有します。
といらえずsrcの中にatoms.jsというファイルを作って、そこに記述します。
touch src/atoms.js
1つの変数につき1atom作る感じです。変数にはもちろんオブジェクトも持てます。
以下ではcountとuserの値を管理する2つのatomを作っています。
import { atom } from "recoil";
export const countState = atom({
key: "count",
default: 0
});
export const userState = atom({
key: "user",
default: {
name: "hoge",
age: 11
}
});
Atomの利用(Home.jsでの表示と更新)
Home.jsを下記のようにしてみます。
import { Link } from "react-router-dom";
import { useRecoilState } from "recoil";
//atoms.jsを読み込み
import { countState, userState } from "./atoms";
const Home = () => {
//useRecoilState(読み書きできる)
const [count, setCount] = useRecoilState(countState);
const [user, setUser] = useRecoilState(userState);
//countをインクリメント
const increment = (c) => {
return c + 1;
}
//user.ageをインクリメント(機能的な意味はなし)
const updateUser = (u) => {
return { ...u, ...{ age: u.age + 1 } };
}
return (
<>
<h1>Home</h1>
<p>count:{count}</p>
<button onClick={() => setCount(increment)}>increment</button>
<hr />
<p>user.name:{user.name}, user.age:{user.age}</p>
<button onClick={() => setUser(updateUser)}>user age increment</button><br/><br/>
<Link to="/about">About</Link>
</>
);
}
export default Home;
実行すると下記のような感じになります。
ボタンを押すとcountやuser.ageがカウントアップされます。
ゴールはこれらの値がAbout.jsでも利用できか?ということです。
Atomの利用(About.jsでの表示:readonly)
About.jsを下記のようにします。
About.jsではuseRecoilStateではなくuseRecoilValueを利用してリードオンリーとしてみました。
ここでは触れませんが、useSetRecoilState()というセットだけのhookもあり、それを利用すると再レンダリングを最小限に抑えることなどができます。
Homeで変更した値が反映されていればOKです。
import { Link } from "react-router-dom";
import { useRecoilValue } from "recoil";
//atom.jsを読み込み
import { countState, userState } from "./atoms";
const About = () => {
//useRecoilValue(読み取るだけ)
const count = useRecoilValue(countState);
const user = useRecoilValue(userState);
return (
<>
<h1>About</h1>
<p>count:{count}</p>
<p>user_name:{user.name}</p>
<p>user_age:{user.age}</p>
<br/>
<Link to="/">Home</Link>
</>
);
}
export default About;
変更した値がAbout.jsでも維持されていればOK。
Recoilの永続化
グローバル(ページ間)で値は共有できるようになりましたが、ブラウザを更新したら値は消えてしまします。
多くのシーンでは値の永続化が不可欠となります。Recoilには永続化の仕組みが備わっているようですが、少々煩雑なのでrecoil-persistというモジュールを利用してみたいと思います。
モジュールのインストール
npm install recoil-persist
atoms.jsに記述を追加
atoms.jsに数行追加するだけで永続化が可能となります。
ここでは2つのatomのうち、userStateだけ永続化の対象としてみます。
import { atom } from "recoil";
import { recoilPersist } from "recoil-persist"; //追加
//標準でrecoil-persistというkey名でwebstorageに保存される(オプションで指定可能)
const { persistAtom } = recoilPersist();
export const countState = atom({
key: "count",
default: 0
});
export const userState = atom({
key: "user",
default: {
name: "hoge",
age: 11
},
effects_UNSTABLE: [persistAtom] //userStateにだけこの記述を追加
});
実行して動作確認してみます。
count, userどちらの値も変更してからブラウザを更新するとuserの方の値だけ保持されています。
WebStorageを確認するとuserの値が保持されているのが確認できます(平文なので保存内容には注意を)。
その他
他にselectorとかもありますが、ここまでで多くの用途に対応できるでしょう。
Reduxも悪くはないですが、recoilの簡単さを知るともう戻れないですね。