概要
Jotai は非常に便利。
色んな機能があるが忘れてしまうので一覧にしておく。
atom
import { atom } from 'jotai'
const priceAtom = atom(10)
const messageAtom = atom('hello')
const productAtom = atom({ id: 12, name: 'good stuff' })
const readOnlyAtom = atom((get) => get(priceAtom) * 2)
const writeOnlyAtom = atom(
null, // null を渡すことで書き込み専用になる
(get, set, newValue) => {
set(priceAtom, get(priceAtom) - newValue.discount)
// もしくは、関数を渡すことも可能
set(priceAtom, (prev) => prev - newValue.discount)
},
)
const readWriteAtom = atom(
(get) => get(priceAtom) * 2,
(get, set, newPrice) => {
set(priceAtom, newPrice / 2)
// 同時に複数のatomをsetすることも可能
},
)
// コンポーネントの中で動的に atom を作るならmemo化必須
const Component = ({ value }) => {
const valueAtom = useMemo(() => atom({ value }), [value])
// ...
}
// onMound: subscribeされたときに常に呼ばれるように
const anAtom = atom(1)
anAtom.onMount = (setAtom) => {
console.log('atom is mounted in provider')
setAtom(c => c + 1) // mount時にインクリメント
return () => { ... } // onUnmount 関数を返すこともできる(オプション)
}
useAtom
const [value, setValue] = useAtom(anAtom)
const value = useAtomValue(anAtom)
const setValue = useSetAtom(anAtom)
atomWithReset / atomWithDefault
初期値に戻せる。
import { atom, useSetAtom } from 'jotai'
import { atomWithReset, RESET } from 'jotai/utils'
const dollarsAtom = atomWithReset(0)
const ResetExample = () => {
const setDollars = useSetAtom(dollarsAtom)
return (
<>
<button onClick={() => setDollars(RESET)}>Reset dollars</button>
</>
)
}
// 初期値は関数でも指定可能
import { atomWithDefault } from 'jotai/utils'
const countAtom = atomWithDefault((get) => get(count1Atom) * 2)
focusAtom
オブジェクトを保持する atom の一部分を別の atom として切り出す。非常に便利。
// npm i optics-ts jotai-optics
const baseAtom = atom({ a: 5 }) // PrimitiveAtom<{a: number}>
const derivedAtom = focusAtom(baseAtom, (optic) => optic.prop('a')) // PrimitiveAtom<number>
atomFamily
キー/バリューでAtomを管理
import { atom } from 'jotai'
import { atomFamily } from 'jotai/utils'
const myFamily = atomFamily(
(id: number) => atom(id),
)
// キーをオブジェクトにすることも可能
const todoFamily = atomFamily(
({ id, name }) => atom({ name }),
(a, b) => a.id === b.id, // 一致を判断するロジック
)
atomWithReducer
値をセットするための特別な命令を用意しておける。
import { atomWithReducer } from 'jotai/utils'
const countReducerAtom = atomWithReducer(0, (prev, action) => {
if (action.type === 'inc') return prev + 1
if (action.type === 'dec') return prev - 1
throw new Error('unknown action type')
})
const countAtom = atom(0)
const Counter = () => {
const [count, dispatch] = useAtom(countAtom)
return (
<div>
{count}
<button onClick={() => dispatch({ type: 'inc' })}>+1</button>
<button onClick={() => dispatch({ type: 'dec' })}>-1</button>
</div>
)
}
splitAtom
配列の要素を個別のAtomで扱う。
import { atom, useAtom } from 'jotai'
import { splitAtom } from 'jotai/utils'
const numsAtomsAtom = splitAtom(atom([1, 2, 3]))
const NumList = () => {
const [numsAtoms, dispatch] = useAtom(todoAtomsAtom)
return (
<ul>
{numsAtoms.map((numAtom) => (
<NumItem
numAtom={numAtom}
remove={() => dispatch({ type: 'remove', atom: numAtom })}
/>
))}
</ul>
)
}
useAtomCallback
コンポーネントの再レンダリングを避けて、atomに値セットできる。
import { useCallback } from 'react'
import { atom, useAtom } from 'jotai'
import { useAtomCallback } from 'jotai/utils'
const countAtom = atom(0)
// useAtomCallback しない場合
const Counter = () => {
// readはしていないがsetCountすれば Counter はレンダリングされる
const [, setCount] = useAtom(countAtom)
const incr = () => setCount(c => c + 1)
return (
<>
<button onClick={incr}>+1</button>
</>
)
}
// ↓
// useAtomCallback する場合
const Counter = () => {
const incr = useAtomCallback(
useCallback((_, set) => {
set(prev => prev + 1)
}, []),
)
return (
<>
<button onClick={incr}>+1</button>
</>
)
}
loadable
Async で初期値を取ることができ、取れるまで仮の状態を返せる。
import { loadable } from "jotai/utils"
const loadableAtom = loadable(atom(async (get) => ...))
// <Suspense> でのラップは不要
const Component = () => {
const [value] = useAtom(loadableAtom)
if (value.state === 'hasError') return <Text>{value.error}</Text>
if (value.state === 'loading') {
return <Text>Loading...</Text>
}
console.log(value.data) // Results of the Promise
return <Text>Value: {value.data}</Text>
}
atomEffect
別の値を監視して追随して更新したい場合。
atom.onMount と違ってマウント時以外でも動く。
// npm i jotai-effect
import { atomEffect } from 'jotai-effect'
const loggingEffect = atomEffect((get, set) => {
// マウント時と someAtom 更新時に走る
const value = get(someAtom)
loggingService.setValue(value)
// return unsubscribe // unsubscribe の関数を返すことも可能
})
const anAtom = atom((get) => {
// anAtom マウント時に atomEffect もマウントされる
get(loggingEffect)
// ...
})
atomWithImmer
ちょっとだけ可読性のよい setValue。オブジェクト全体を返す必要がない。オブジェクトを更新するだけでよい。
import { useAtom } from 'jotai'
import { atomWithImmer } from 'jotai-immer'
const countAtom = atomWithImmer({ value: 0 })
const Counter = () => {
const [count, setCount] = useAtom(countAtom)
const inc = () => {
// setCount(prev => {{ ...prev, value: prev.value + 1 }})
// ↓
setCount(draft => { ++draft.value })
}
return (
<>
<div>count: {count.value}</div>
<button onClick={inc}>+1</button>
</>
)
}
atomWithMachine
ステートマシン
// npm i xstate jotai-xstate
import { useAtom } from 'jotai'
import { atomWithMachine } from 'jotai-xstate'
import { assign, createMachine } from 'xstate'
// ステートマシンの生成
const createEditableMachine = (value: string) =>
createMachine<{ value: string }>({
id: 'editable',
initial: 'reading', // 最初のステート
context: {
value, // 内包する値
},
states: {
reading: {
on: {
dblclick: 'editing', // イベントでステート変更する
},
},
editing: {
on: {
cancel: 'reading', // イベントでステート変更
commit: { // イベントで追加のアクションも行う
target: 'reading',
actions: assign({ // context の値を更新
value: (_, { value }) => value,
}),
},
},
},
},
})
const defaultTextAtom = atom('edit me')
const editableMachineAtom = atomWithMachine((get) =>
// ステートマシンの初期化では `get` だけが利用可能
createEditableMachine(get(defaultTextAtom)),
)
const Toggle = () => {
const [state, send] = useAtom(editableMachineAtom)
return (
<div>
// ↓ matches でステートの名前を比較
{state.matches('reading') && (
// ↓ 標準のイベントハンドラとして send を渡せる
<strong onDoubleClick={send}>{state.context.value}</strong>
)}
{state.matches('editing') && (
<input
autoFocus
defaultValue={state.context.value}
// ↓ 独自のイベントを send で送る
onBlur={(e) => send({ type: 'commit', value: e.target.value })}
onKeyDown={(e) => {
if (e.key === 'Enter') {
send({ type: 'commit', value: e.target.value })
}
if (e.key === 'Escape') {
send('cancel')
}
}}
/>
)}
<br />
<br />
<div>
Double-click to edit. Blur the input or press <code>enter</code> to
commit. Press <code>esc</code> to cancel.
</div>
</div>
)
}