本日は、グローバルな状態をRecoilで扱ったらどんな風に書けるのか、
ドキュメントを読みながら実際にサンプルコードを書いてみたのでシェアします。
コードはこちらに載せています。
https://github.com/zhizit/recoil-practice
今回、「南極忘年会」の仕様としては以下のように考えました。
- 主催にかかる費用を設定できる
- 割り勘表にゲストを追加・削除ができる
- 追加された人数(匹?w)に応じて1人あたりの支払い金額を算出する
- 主賓は500円オフで、その分を主賓以外が割り勘する
- 1円単位だと小銭がバラバラするので細かいお金にならないよう100円単位で扱う
セットアップ
まずはセットアップですね。Recoil をインストールしていきます。
# npm
$ npm install recoil
# yarn
$ yarn add recoil
こんな感じでRecoilRootを実装します。
import React from 'react'
import ReactDOM from 'react-dom/client'
import { RecoilRoot } from 'recoil'
import App from './App'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>
)
AtomとSelector
Recoil には Atom と Selector という概念があります。
Atom
Atom とは状態の単位のことを指していて、Atom が更新されると subscribe しているコンポーネントが再レンダリングする仕組みになっています。同じ Atom が複数のコンポーネントで使われるとき、すべてのコンポーネントがその状態をグローバルな状態として共有します。
今回のサンプルだと予算とゲストのリストを定義しています。
export const arrangementPriceState = atom({
// UniqueになるようにKeyを設定
key: "ArrangementPrice",
default: 13200,
});
export const personsState = atom({
key: "Persons",
default: [
{ id: 1000, name: "ペンギン", isMainGuest: false },
{ id: 1001, name: "トナカイ", isMainGuest: false },
{ id: 1002, name: "ベルーガ", isMainGuest: false },
{ id: 1003, name: "シロクマ", isMainGuest: true },
],
});
Selector
Atom にもとづく派生 state で、Atom の値を何らかの計算や加工した結果を返します。
import { selector } from "recoil";
// atomで定義したstate
import { personsState, arrangementPriceState } from "./atom";
// 主賓の人数を扱うStateとしてselectorを生成
export const mainGuestCountState = selector({
// UniqueになるようにKeyを設定
key: "MainGuestCount",
get: ({ get }) => {
// get(定義したState) でatomのアイテムを取得できる
const persons = get(personsState);
// 加工して返す
return persons.filter((person) => person.isMainGuest).length;
},
});
// 主賓ではないゲストの人数を扱うStateとしてselectorを生成
export const guestCountState = selector({
key: "GuestCount",
get: ({ get }) =>
get(personsState).filter((person) => !person.isMainGuest).length,
});
// このように、使いたい用途に合わせて細かく定義が可能!
export const regularBillState = selector({
key: "RegularBill",
get: ({ get }) => {
const arrangementPrice = get(arrangementPriceState);
if (arrangementPrice < 1000) return 0;
// 主賓は500円OFFなので、他のゲストが多めに払う
const mainGuestCount = get(mainGuestCountState);
const extraPrice = mainGuestCount * 500;
const price =
(arrangementPrice + extraPrice) / (get(guestCountState) + mainGuestCount);
// 細かいお金にならないよう100円単位で請求
return Math.ceil(price / 100) * 100;
},
});
export const specialDiscountBillState = selector({
key: "SpecialDiscountBill",
get: ({ get }) => {
const bill = get(regularBillState);
return bill - 500 >= 0 ? bill - 500 : 0;
},
});
// 以下のように複数のStateをまとめて書くこともできる
export const totalState = selector({
key: "Total",
get: ({ get }) => {
const personCount = get(mainGuestCountState) + get(guestCountState);
const actualAmount =
get(regularBillState) * get(guestCountState) +
get(specialDiscountBillState) * get(mainGuestCountState);
return {
personCount,
actualAmount
}
},
});
準備ができたら Atom や Selector から値を取得したり、更新してみましょう。
Recoil には様々な hook が用意されています。
Atom: https://recoiljs.org/docs/api-reference/core/atom/
Selector: https://recoiljs.org/docs/api-reference/core/selector/
今回は、useRecoilState, useRecoilValue, useSetRecoilState を使いました。
useRecoilState
最初の要素が状態の値で、2 番目の要素が呼び出されたときに指定された状態の値を更新するセッター関数であるタプルを返します。
この hook は、コンポーネントを subscribe して、要求された状態の変更を再レンダリングします。
useRecoilValue
この hook は、Recoil の状態が変化した場合にコンポーネントを subscribe して再レンダリングします。
useSetRecoilState
この hook は、書き込み可能な Recoil 状態の値を更新するためのセッター関数を返します。
useRecoilState とは異なり、更新することに特化していて subscribe はしないため、Recoil の状態が変化しても再レンダリングされることはありません。
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
import { PersonalBill } from "./components/PersonalBill";
import { personsState, arrangementPriceState } from "./recoil/atom";
import { totalState } from "./recoil/selector";
import "./App.css";
export type Person = {
id: number;
name: string;
isMainGuest: boolean;
};
function App() {
// useRecoilState hook を使うと値の参照と登録ができる
const [persons, setPersons] = useRecoilState(personsState);
// useRecoilValue hook を使うと値を参照できる
const { personCount, actualAmount } = useRecoilValue(totalState);
// useSetRecoilState hook を使うと値を追加できる
const setArrangementPrice = useSetRecoilState(arrangementPriceState);
const handleAddPerson = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setPersons((oldList) => [
...oldList,
{
id: getId(),
name: event.target.name.value,
isMainGuest: event.target.isMainGuest.checked,
},
]);
};
const onChangeArrangementPrice = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setArrangementPrice(Number(event.target.value));
};
return (
<div className="App">
<h2>南極忘年会</h2>
<h3>費用として集めたい金額の目安</h3>
<div>
<label>
¥:
<input
type="number"
min="0"
onChange={onChangeArrangementPrice}
// WARNING: ちゃんと書くならAtomから取得するべき。useSetRecoilStateのsampleを書くためにこうしている。
defaultValue="13200"
/>
</label>
</div>
<h3>割り勘表</h3>
<div>
<p>※主賓は500円OFF</p>
<p>※細かいお金にならないよう100円単位で請求</p>
{persons.map((person) => (
<PersonalBill key={person.id} person={person} />
))}
<p>
{personCount}人:合計¥
{actualAmount.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,")}
</p>
</div>
<h3>割り勘表に追加</h3>
<form onSubmit={handleAddPerson}>
<label>
<input type="checkbox" name="isMainGuest" />
主賓
</label>
<label>
<input type="text" name="name" placeholder="名前" required />
</label>
<input type="submit" value="決定" />
</form>
</div>
);
}
export default App;
// utility for creating unique Id
let id = 1;
function getId() {
return id++;
}
以上で、デモのような仕様を表現できました!
かなりシンプルですよね!
Flux や Redux は学習コストが高く、単一方向のデータフローを実現するのにコード量も多かったので、Recoil の手軽さにトキメキました!
他にも hook がたくさんあるので、またサンプルを書いてどんな動きになるのか、試して見ようと思います!