LoginSignup
0
0

jotai の atom の機能一覧

Last updated at Posted at 2024-02-17

概要

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>
  )
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0