core
atom生成
- atom
- atomFamily
- atomFrozenInDev
- atomWithImmer
- atomWithReducer
- atomWithReset
- withImmer
atom使用
- useAtom
- useAtomCallback 再描画抑止
- useAtomValue
- useBridge
- useImmerAtom
- useReducerAtom
- useResetAtom
- useSelector 廃止 (selectAtomに変更)
- useUpdateAtom 再描画抑止
特殊系
- focusAtom
- freezeAtom
- selectAtom
atom
それ自体には状態を持たず、値はProviderに保存される。
参照するコンポーネントが全てunMountされるとProviderの値も消える。
どこで作っても良くて(コンポーネント内外問わず)、Reactのstateに入れても良い。
debugLabel
React Dev Toolsを使い、Providerを見るとDebugState
という項目があり、ここにatomの値が確認できる。
debugLabelを使わないとatomの名前は連番になるのでどれがどれか分かりにくい。この為debugLabelを使ってわかりやすく出来る。
onMount
const anAtom = atom(1)
anAtom.onMount = (setAtom) => {
console.log('returnするまではonMountで実行される')
setAtom(c => c + 1) // 初期値の設定などに便利
return () => { ... } // unMount時に実行される
}
これを指定すると、そのatomが最初にProviderに保存される時に実行される。
全ての参照するコンポーネントが無くなるとunMountが実行される(return () => ...
は必要がなければ返さなくていい void可)。
Provider
atomの値が保存される場所。基本コンポーネントRootに一つあれば良い。
initialValues
初期値の指定が可能。
scope
状態管理上、複数Providerを持ちたい場合はscopeを指定してやれば、それぞれのProvider対atomで管理できる。
useAtom
React.useStateに似てる使い方。
const anAtom = atom(1, (get, set, update) => {...})
...
const [value, setValue] = useAtom(anAtom)
ここで言うsetValue
は1つの値だけを取り、それが(get, set, update) => {...}
のupdate
部分に渡されることになる。
useBridge/Bridge
よく分からない。
utils
useUpdateAtom
import { atom, Provider, useAtom } from "jotai"
import React from "react"
const countAtom = atom(0)
interface Props {}
const CounterDisplay: React.FC<Props> = (props) => {
console.log("CounterDisplay:")
const [count] = useAtom(countAtom)
return <>{count}</>
}
const Counter: React.FC<Props> = (props) => {
console.log("Counter:")
const [, set] = useAtom(countAtom)
return (
<button
onClick={() => {
set((prev) => prev + 1)
}}
>
up
</button>
)
}
const index: React.FC<Props> = (props) => {
console.log("index:")
return (
<Provider>
<CounterDisplay />
<Counter />
</Provider>
)
}
export default index
こんな感じで同じatomを別のコンポーネントで参照していて、一方は値のみ参照、もう一方は値の更新のみ担当みたいな感じで使っている場合
- ボタンを押す
- イベントが発生
- setを実行
- countが更新される
- CounterDisplayコンポーネントが再描画
- Counterコンポーネントも再描画(atomが更新されているので...)
6.が無駄だよねってことで
import { useUpdateAtom } from "jotai/utils"
...
const Counter: React.FC<Props> = (props) => {
const set = useUpdateAtom(countAtom)
...
のようにアップデート関数だけを抽出しておけば、atomの値の変化にコンポーネントが反応しない=不必要な再描画がない。
多岐にわたるコンポーネントで同じatomを使用する場合、再描画を抑えるために出来るだけatomはatom系で作っておいて、useUpdateAtomを使えるところは使ったほうが良さそう。
atomWithReducer例
先にatomがあって、useReducerAtomを使うとuseUpdateAtomは併用できない
const reducer = (prev, action) => {
if (action.type === "inc") return prev + 1
if (action.type === "dec") return prev - 1
throw new Error("unknown action type")
}
const counter = atomWithReducer(0, reducer)
const A: React.FC<Props> = (props) => {
console.log("A:")
const [count, dispatch] = useAtom(counter)
...
}
const B: React.FC<Props> = (props) => {
console.log("B:")
// ここで普通にこうしてしまうと、Aでdispatchされた際にこのBも再描画される
// const [, dispatch] = useAtom(counter)
const dispatch = useUpdateAtom(counter)
return (
...
}
useAtomValue
useUpdateAtomはアップデート関数だけを抽出するのに対して、useAtomValueは値だけを抽出する(Recoilに合わせたAPIらしいが、再描画を抑えるためではないと思う、シンタックスシュガー的な?)。
const [value, ] = useAtom(atom)
でも同じ気がする... (よく見てみたら実際にそう)
atomWithReset
atom生成時とかに初期値設定してても、一旦変更した後、「あー初期値に戻したい」ってなったら結構面倒くさくない?
という事で初期値の戻せるatomがあれば良いよねという感じのatom。
useResetAtomと併用する必要がある。
useResetAtom
atomWithResetのリセット機能付きatomを、実際にリセット(初期値に戻す)時に実行するfunctionを生成するフック。
import { atomWithReset, useResetAtom } from 'jotai/utils'
const count = atomWithReset(100)
...
const reset = useResetAtom(count)
...
<button onClick={() => { reset() }}>初期値(100)に戻す</button>
RESET
atomWithResetで作ったatomならRESETという特殊なSymbolが使えて、これを元に独自のリセットを実装できる。
useReducerAtom
atomを使ってreducerを簡単に作れるフック。reducerで返す値がatomのsetにそのまま渡されるので、返す値=新たな値。
atomWithReducer
やってることはuseReducerAtomと同じだけど、
useReducerAtom
- atomを作る
- useReducerAtomにこのatomとreducerを指定する
atomWithReducer
- atomを作ると同時にreducerも指定する
atomを作る時に最初からreducer使うことが分かっているのなら、atomWithReducerを使うかどうかの違いと思う。
atomFamily
Mapのようにatomを束ねて一纏めにしたい時に便利。(TODOリスト的な不特定多数なTODOを管理したい時とか)
引数は
- [必須] 保存されるatomとなるものの初期値をfunctionで指定 参照(
atom(defaultValue)
部分と同じ) - atomをどういう風にアップデートするか(
atom(..., (get, set, update) => {...})
部分と同じ) 参照 - 読み出す際にどいう言う比較して、一致する値とするか 参照
// 1.の部分 | 2.の部分
const anAtom = atom(1, (get, set, update) => {...})
最初に入れた値がMapのキーになる。
import { atomFamily } from 'jotai/utils'
const todoFamily = atomFamily((name) => name)
todoFamily('foo') // => foo
todoFamily({id: "abc", value: 123}) // => {id: "abc", value: 123}
ドキュメントに書かれていはいないが.remove
でfamily内の特定のatomを削除できる。
const aFamily = atomFamily((x) => x)
aFamily('foo')
aFamily.remove('foo')
const param = {id: "abc", value: 123}
aFamily(param)
aFamily.remove(param) // 比較する関数を指定していないので、削除する時は全く同じオブジェクトが必要(内部でMapを使っている)
const bFamily = atomFamily((obj) => obj, null, (a, b) => a.id === b.id)
bFamily({id: "abc", value: 123})
bFamily.remove({id: "abc"}) // 比較する関数を指定しているので、idだけ渡せばこの場合はOK
また、.setShouldRemove
というのもある。これは通常使う必要はなく、どうしても独自のガーベッジコレクション的なものを実装したい場合に設定する。
(基本的にfamily追加時のミリ秒を持っているので、それで比較するなど...)
useSelector 廃止(selectAtomが追加)
useSelector(anAtom, selector, equalityFn)
複数のメンバーを持つオブジェクトをatomにしてる場合、一部が更新されたら全体を参照するのではなく、一部の更新は一部のみで対応して再描画を抑えたい時に便利。
内部でuseMemo
のdependenciesに使っているので、selector, equalityFn
はコンポーネントの外に書ければそうしたほうが良い。そうじゃなければuseCallback
を使って逐次オブジェクトとして変わらないようにしなければ再描画が常に行われることになってしまう。
dependenciesが多くてuseCallbackとか気を使うのであれば、後から出てくるfocusAtomのほうが楽かも。
selectAtom
useSelectorと基本同じだけど、フックではなくatomに変更。
useAtomCallback
フックが使えないコンポーネント以外でatom使いたくない?
って言うことから追加されたけど、結局useフックなのでコンポーネントの中からしか使えない...のはなぜ。
多岐にわたるコンポーネントで同じatomを参照している場合、atomの値が変更されるとどうしても使用している全てのコンポーネントの再描画が起きてしまう。
このフックを使えば、値の変更を随時表示させるとか計算させる 必要がない(再描画不要) んだけど、値の参照はクリックイベントの時だけしたい、とか出来る。(useUpdateAtomの値版というか...)
これもuseMemo
を使っているので、渡すcallback関数はコンポーネント再描画毎に逐次変わらないようにuseCallback使わないと無駄な再描画が起きる。
callbackをラップしたPromiseを返してくるので、async/awaitを使わないといけない
import { atom, Provider, useAtom } from "jotai"
import { useAtomCallback } from "jotai/utils"
import React from "react"
const a = atom(0)
const A: React.FC<Props> = (props) => {
console.log("A:")
const [count, set] = useAtom(a)
return (
<>
{count}
<button
onClick={() => {
set((prev) => prev + 1)
}}
>
up
</button>
</>
)
}
const B: React.FC<Props> = (props) => {
console.log("B:")
const callback = useAtomCallback(React.useCallback((get) => get(a), []))
return (
<>
<button
onClick={async () => {
console.log("callback():", await callback())
}}
>
call back
</button>
</>
)
}
interface Props {}
const index: React.FC<Props> = (props) => {
console.log("index:")
return (
<>
<Provider>
<A />
<B />
</Provider>
</>
)
}
export default index
因みにドキュメントには書かれていないが、setも出来るし、callbackにパラメータを渡すことも出来る。
const callback = useAtomCallback(
React.useCallback((get, set, arg) => {
console.log("set:", set) // atomをsetしようと思えば出来る
console.log("arg:", arg) // callback(1)すれば、1が来る
return get(a)
}, [])
)
...
<button
onClick={async () => {
console.log("callback():", await callback(1))
}}
>
call back
</button>
freezeAtom
atomをimmutableにしてしまい、デバックしやすくするため。
atomにオブジェクトとかを入れていたとして、どこかで期待以外の値に変更されるなどが起きた場合、そのイベント事前に固まらせておいて、エラーを起こさせてデバックできるとか?
atomFrozenInDev
freezeAtomと同じだけど、NODE_ENV=developmentの時だけ固まる。
immer
使うには別途immerが必要。
yarn add immer
atomWithImmer
immerを使ったことがあるならわかるけど、複雑なオブジェクトをimmutableとして扱わずにガンガン変更して、そのままでいいので便利。
import { atomWithImmer } from "jotai/immer"
const counterAtom = atomWithImmer({ count: 0 })
const Counter: React.FC<Props> = (props) => {
const [counter, setCounter] = useAtom(counterAtom)
return (
<button
onClick={() => {
// 通常だと新しいオブジェクトが必要
// setCounter((counter) => {
// const temp = {...counter}
// temp.count += 1
// return temp
// })
setCounter((counter) => {
counter.count += 1
// オブジェクトなら値を変更して何も返さないでいい、返してもいいけど
// プリミティブな値ならその値を返す
})
}}
>
{counter.count}
</button>
)
}
withImmer
atomWithImmerと基本同じだが、すでにあるatomをImmer化したatomを返す。
useImmerAtom
通常のatomをimmer化したものとして使う。
※atomWithImmerやwithImmerでimmer化したatomと併用すべきではない。
const counterAtom = atomWithImmer(0)
const regularAtom = atom(0)
const immeredRegularAtom = withImmer(regularAtom)
...
const [value, setValue] = useImmerAtom(counterAtom) // 良くない
const [value, setValue] = useImmerAtom(regularAtom) // これは良い
const [value, setValue] = useImmerAtom(immeredRegularAtom) // 良くない
optics
atomを一緒くたに使うのは簡単だけど、バラすのはどう?、と言うのから出来たoptics。
使うには別途optics-tsが必要。
yarn add optics-ts
複雑な階層を持つオブジェクトをatomとした場合、全体を変更するのではなく、パーツパーツで切り出してそれぞれを個別のatomにしてしまえば管理しやすいよね?的な。(useSelector
は参照のみだが、こちらは参照と変更ができる。)
切り出したとて大元のatomへも変更の通知は行くので便利。
focusAtom
例は単純に切り出しただけだけど、optics-tsは色々出来る(Lens、Prism、Isomorphism [用語が独特])んで要参照。
import { atom } from 'jotai'
import { focusAtom } from 'jotai/optics'
const objectAtom = atom({ a: 5, b: 10 })
const aAtom = focusAtom(objectAtom, (optic) => optic.prop('a')) // 切り出す
const bAtom = focusAtom(objectAtom, (optic) => optic.prop('b')) // 切り出す
const Controls = () => {
// a, bそれぞれ変更しても、objectAtomも逐次変更される
const [a, setA] = useAtom(aAtom)
const [b, setB] = useAtom(bAtom)
return (
<div>
<span>Value of a: {a}</span>
<span>Value of b: {b}</span>
<button onClick={() => setA((oldA) => oldA + 1)}>Increment a</button>
<button onClick={() => setB((oldB) => oldB + 1)}>Increment b</button>
</div>
)
}