背景
State管理ライブラリであるJotaiを使っているのですが、なんもわからんので将来わかることを見越してメモしておきます。
Async
atom
これはユーザーのデータなどを固定値のURLを呼び出してデータを取ってくるときに使えます。
const userAtom = atom((get) => {
const response = await fetch(new URL(...).href)
return await response.json()
})
これはfetch
中にはSuspense
のfallback
のコンポーネントが呼び出されるのでユーザーに「現在通信中である」ということを簡単に通知できます。
よって、Suspense
を利用する必要があります。
const View: React.FC = () => {
const user: View = useAtomValue(userAtom) return (
<p>{user.name}</p>
)
}
で、気になるのはもしユーザー情報を更新したい場合にはどうするかということですが単純な非同期atomにはset
がないのでこれは実現できません。
要するに、一回値が入るとキャッシュが利用されてしまうので二度と更新できません。
loadable
非同期atomの使い方の一つでSuspense
やError Boundary
を使いたくないときに使います。
基本的にはSuspense
やError Boundary
を使うと思うのでこちらは解説しません。
atomWithObservable
rxjs
と組み合わせる感じです。
const counterAtom = atomWithObservable(
() => interval(1000).pipe(map((value) => value * 2))
);
ちょっと調べてみた感じvalue
は0から順番に数値が返ってくるみたいですね。
const View: React.FC = () => {
const [counter] = useAtom(counterAtom);
return (
<>
<ModalDialog>
<DialogTitle>Asnyc</DialogTitle>
<DialogContent>
<button type="button">{counter}</button>
</DialogContent>
</ModalDialog>
</>
);
};
なのでこう書くと0, 2, 4, 8という感じで一秒ごとにデータが更新されます。
unwrap
非同期atomの使い方の一つでSuspense
やError Boundary
を使いたくないときに使います。
基本的にはSuspense
やError Boundary
を使うと思うのでこちらは解説しません。
atomWithRefresh
const userAtom = atomWithRefresh((get) => {
const response = await fetch(new URL(...).href)
return await response.json()
})
これは非同期atomに対してリフレッシュ機能を提供します。
const View: React.FC = () => {
const [user, refresh] = useAtom(userAtom)
return (
<button onClick={() => refresh()}>{user.name}</button>
)
}
この場合だとボタンを押したときにuserAtom
の再評価が行われ、便利なわけですね。
公式ドキュメントだとonClick={refresh}
だけで更新がかかるような書き方がされていますが、Vite+Reactの環境では動きませんでした。
Lazy
遅延評価をするatomです。
似たような仕組みがSwiftにもあるので多分同じ感じだと思います。
atomWithLazy
ですがググっても全然出てこない上に公式ドキュメントも良くわからないのでスキップします。
Reset
atomWithReset
初期値を再代入するためのatomです。
RESET
という特別な値を入れることで初期化できます。
これもドキュメントが良くわからないのでまだ使えていません。
atomWithDefault
再代入かつ初期化できるatomです。
const count1Atom = atom(1);
const count2Atom = atomWithDefault((get) => get(count1Atom) * 2);
例えばこう書いたとすると初期値はそれぞれ1と2になります。
ただ、count2Atom
の初期値がcount1Atom
の二倍にしたいのであれば、
const count1Atom = atom(1);
const count2Atom = atom((get) => get(count1Atom) * 2);
こう書いても同じ結果が得られます。ただし、これはReadOnlyなatomなので再代入ができません。
const View: React.FC = () => {
const [count1, setCount1] = useAtom(count1Atom);
const [count2, setCount2] = useAtom(count2Atom);
return (
<div>
count1: {count1}, count2: {count2}
</div>
<button type='button' onClick={() => setCount1((c) => c + 1)}>
increment count1
</button>
<button type='button' onClick={() => setCount2((c) => c + 1)}>
increment count2
</button>
<button type='button' onClick={() => setCount2(RESET)}>
Reset with RESET const
</button>
)
}
こう書くとcount2
を変更しない間は初期値が利用されるのでcount1
を増加させるとその二倍の値がcount2
として設定されます。
そこからcount2
だけを増加させるとcount1
を増やしてもこれら二つは同期されなくなりますが、リセットを実行するとcount2
はcount1
の値の二倍(初期値)に戻ります。
const [count2, setCount2] = useAtom(count2Atom);
const resetCount2 = useResetAtom(count2Atom);
という風に定義すればresetCount2()
とsetCount2(RESET)
は同じ効果を持ちます。
Storage
値をLocalStorageなどに保存して永続化することができます。
主に設定項目などを保存しておくと良いと思います。
LocalStorageは暗号化されていないのでクレデンシャルなどの情報は保存してはいけません。
const darkModeAtom = atomWithStorage("theme", false);
キー名を指定して値を保存します。このとき、デフォルトだとJSON.stringify()
が利用されるのでJSONに変換できない値は保存できません。保存したい場合には自分で保存用のメソッドを定義する必要があります。
また、何も指定しない場合にはLocalStorageから読み込む前に第一引数(この場合はfalse)で初期化されます。
const View: React.FC = () => {
const [darkMode, setDarkMode] = useAtom(darkModeAtom);
return (
<button type="button" onClick={() => setDarkMode(!darkMode)}>
{darkMode ? "Dark Mode" : "Light Mode"}
</button>
)
}
よってこのようにコンポーネントを定義した場合、どのような値で保存されていたとしても最初にLight Mode
と表示されてしまいます。
これが意図した挙動と違うのであれば、
const darkModeAtom = atomWithStorage("theme", false, undefined, {
getOnInit: true,
});
このように第三引数のオプションにgetOnInit
でtrue
を指定すれば値が保存されていれば初期値の代わりにLocalStorageから読み込んだ値で初期化します。
こっちの方が挙動として自然だと思うので、こっちを使ったほうがいいと思います。
Family
atomFamily
atomFamily
は引数を受け取ってatomを返すメソッドを提供します。
type User = {
name: string
age: number
}
const usersAtomFamily = atomFamily(
({ name, age}: User) => atom({ name: name, age: age}),
(a,b) => a.name === b.name
)
これは無名のatomを生成し、Jotaiの内部状態として保存します。よって、外部からatomの管理をしなくていいというのが便利です。
const usersAtom = atom<User[]>([])
じゃあこれと変わらないんじゃないかと思うかもしれませんが、atomFamily
の場合はそれぞれ個別のatomを保存しているため要素の一つの値が変化したときにその値に関するコンポーネントだけ再レンダリングが走ります。
それに対して要素の配列自体をatomに入れた場合はどれか一つの値が変わると全ての要素に関するコンポーネントが再レンダリングされるため、例えば要素が100あるatomが変化したとき前者では変化したatomの分だけしかコンポーネントが変化しませんが、後者の場合は全てのコンポーネントが再描画されるため動作が重くなるなどの懸念が発生します。
よって、配列の一部が変化する可能性がある変数をatomとして管理する場合にはatomFamily
を利用したほうが効率が良いことがあります。
唯一困る点としてはatomFamily
は保存している内部状態全ての値を一度に取得するプロパティがありません。
よって、もし全件をループしてコンポーネントとして表示したい場合には保存しているatomのユニークなキーを別のatomとして保存しておく必要があります。
Callback
const countAtom = atom<number>(0)
まずは適当に数値をカウントするためのatomを作成します。
const Counter: React.FC = () => {
const [count, setCount] = useAtom(countAtom)
return (
<>
<div>ATOM: {count}</div>
<button type="button" onClick={() => setCount((c) => c + 1)}>
+1
</button>
</>
)
}
const View: React.FC = () => {
const [count, setCount] = useState(0)
const readCount = useAtomCallback(
useCallback((get) => {
const current: number = get(countAtom);
console.log("CURRENT:", current);
setCount(current);
return current;
}, []),
);
// readCountが初期化された段階で呼ばれる
useEffect(() => {
const timer = setInterval(async () => {
console.log(readCount());
}, 1000);
return () => {
clearInterval(timer);
};
}, [readCount]);
return (
<div>CALLBACK: {count}</div>
<Counter />
)
}
こう書くとボタンを押せばcountAtomの状態が変わってどんどん値が変わるのは当たり前なのですが、このときuseAtomCallback
はどういう挙動をするのかが気になります。
で、どうなっているかというと一秒に一回readCount
が呼び出されてその時にcountAtomの値を取得してその値をcountに代入します。
よって、ボタンを連打していても一秒に一回、countAtomの値とcountの値が同期されるというわけです。
この機能自体は通常のatomを使って、
const Monitor: React.FC = () => {
const [count, setCount] = useState(0);
const _count = useAtomValue(countAtom);
useEffect(() => {
setCount(_count);
}, [_count]);
return <div>CALLBACK: {count}</div>;
};
こう書けば同期的に実行できるのにわざわざuseAtomCallbackを使うのはLocalStorageやAPI通信などの非同期通信を伴う場合、即座に変更させることができないからですね。
まとめ
まだまだ途中までしかかけていませんが、これからいろいろJotaiについて調べていこうと思います。