jotai の atom の機能一覧

# 概要

[Jotai](https://jotai.org/) は非常に便利。
色んな機能があるが忘れてしまうので一覧にしておく。



# [atom](https://jotai.org/docs/core/atom)

```ts
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](https://jotai.org/docs/core/use-atom)

```ts
const [value, setValue] = useAtom(anAtom)
const value = useAtomValue(anAtom)
const setValue = useSetAtom(anAtom)
```



# [atomWithReset / atomWithDefault](https://jotai.org/docs/utilities/resettable)

初期値に戻せる。

```ts
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](https://jotai.org/docs/extensions/optics)

オブジェクトを保持する atom の一部分を別の atom として切り出す。非常に便利。

```ts
// 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](https://jotai.org/docs/utilities/family)

キー/バリューでAtomを管理

```ts
import { atom } from 'jotai'
import { atomFamily } from 'jotai/utils'

const myFamily = atomFamily(
  (id: number) => atom(id),
)
// use 方法
const [myValue, setMyValue] = useAtom(myFamily(id))

// キーをオブジェクトにすることも可能
const todoFamily = atomFamily(
  ({ id, name }) => atom({ name }),
  (a, b) => a.id === b.id, // 一致を判断するロジック
)
// use 方法
const [todo, setTodo] = useAtom(todoFamily({ id, name }))
```



# [atomWithReducer](https://jotai.org/docs/utilities/reducer)

値をセットするための特別な命令を用意しておける。

```ts
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](https://jotai.org/docs/utilities/split)

配列の要素を個別のAtomで扱う。

```ts
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](https://jotai.org/docs/utilities/callback)

コンポーネントの再レンダリングを避けて、atomに値セットできる。

```ts
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](https://jotai.org/docs/utilities/async)

Async で初期値を取ることができ、取れるまで仮の状態を返せる。

```ts
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](https://jotai.org/docs/extensions/effect)

別の値を監視して追随して更新したい場合。
atom.onMount と違ってマウント時以外でも動く。

```ts
// 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 の関数を返すことも可能
})
```

```ts
const anAtom = atom((get) => {
  // anAtom マウント時に atomEffect もマウントされる
  get(loggingEffect)
  // ...
})
```



# [atomWithImmer](https://jotai.org/docs/extensions/immer)

ちょっとだけ可読性のよい setValue。オブジェクト全体を返す必要がない。オブジェクトを更新するだけでよい。

```ts
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](https://jotai.org/docs/extensions/xstate)

ステートマシン

```ts
// 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>
  )
}
```