SuperAPI AI Directory

jotai-expert

Frontend & websites

View repository

Updated 10-16 09:54

About this skill

Jotai Expert

Implementation guide for React state management using Jotai.

Core Concepts

Atom

The smallest unit of state. It holds no value and is stored in the Store.

// Primitive atom
const countAtom = atom(0)
const nameAtom = atom('')

// Derived read-only atom
const doubleAtom = atom((get) => get(countAtom) * 2)

// Derived read-write atom const countWithLabelAtom = atom( (get) => Count: ${get(countAtom)}, (get, set, newValue: number) => set(countAtom, newValue) )

// Write-only atom (action atom) const incrementAtom = atom(null, (get, set) => { set(countAtom, get(countAtom) + 1) })


### Hooks

```typescript
// Read and write
const [value, setValue] = useAtom(countAtom)

// Read only
const value = useAtomValue(countAtom)

// Write only
const setValue = useSetAtom(countAtom)

Implementation Patterns

Pattern 1: Feature Module

// atoms/user.ts
const baseUserAtom = atom<User | null>(null)

// Public read-only atom
export const userAtom = atom((get) => get(baseUserAtom))

// Actions
export const setUserAtom = atom(null, (get, set, user: User) => {
  set(baseUserAtom, user)
})

export const clearUserAtom = atom(null, (get, set) => {
  set(baseUserAtom, null)
})

Pattern 2: Async Data Fetching

const userIdAtom = atom<number | null>(null)

// Suspenseと連携する非同期atom
const userDataAtom = atom(async (get) => {
  const userId = get(userIdAtom)
  if (!userId) return null
  const response = await fetch(`/api/users/${userId}`)
  return response.json()
})

// Component
function UserProfile() {
  const userData = useAtomValue(userDataAtom)
  return <div>{userData?.name}</div>
}

// Suspenseでラップ
<Suspense fallback={<Loading />}>
  <UserProfile />
</Suspense>

Pattern 3: atomFamily

Dynamically generates and caches atoms. Memory leak countermeasures are mandatory.

const todoFamily = atomFamily((id: string) =>
  atom({ id, text: '', completed: false })
)

// 使用
const todoAtom = todoFamily('todo-1')

// クリーンアップ
todoFamily.remove('todo-1')

// 自動削除ルール設定
todoFamily.setShouldRemove((createdAt, param) => {
  return Date.now() - createdAt > 60 * 60 * 1000 // 1時間後削除
})

Pattern 4: Persistence

import { atomWithStorage } from 'jotai/utils'

// localStorage永続化
const themeAtom = atomWithStorage('theme', 'light')

// sessionStorage永続化
import { createJSONStorage } from 'jotai/utils'
const sessionAtom = atomWithStorage(
  'session',
  null,
  createJSONStorage(() => sessionStorage)
)

Pattern 5: Reset

import { atomWithReset, useResetAtom, RESET } from 'jotai/utils'

const formAtom = atomWithReset({ name: '', email: '' })

// コンポーネント内
const resetForm = useResetAtom(formAtom)
resetForm() // 初期値に戻る

// 派生atomでRESETシンボル使用
const derivedAtom = atom(
  (get) => get(formAtom),
  (get, set, newValue) => {
    set(formAtom, newValue === RESET ? RESET : newValue)
  }
)

Performance Optimization

selectAtom

Retrieves only part of a large object. Prefer derived atoms, and use only when necessary.

import { selectAtom } from 'jotai/utils'

const personAtom = atom({ name: 'John', age: 30, address: {...} })

// nameのみを購読
const nameAtom = selectAtom(personAtom, (person) => person.name)

// 安定した参照が必要(useMemoまたは外部定義)
const stableNameAtom = useMemo(
  () => selectAtom(personAtom, (p) => p.name),
  []
)

splitAtom

Manages each element of an array as an independent atom.

import { splitAtom } from 'jotai/utils'

const todosAtom = atom<Todo[]>([])
const todoAtomsAtom = splitAtom(todosAtom)

function TodoList() {
  const [todoAtoms, dispatch] = useAtom(todoAtomsAtom)

  return (
    <>
      {todoAtoms.map((todoAtom) => (
        <TodoItem
          key={`${todoAtom}`}
          todoAtom={todoAtom}
          onRemove={() => dispatch({ type: 'remove', atom: todoAtom })}
        />
      ))}
    </>
  )
}

TypeScript

// 型推論を活用(明示的型定義は不要な場合が多い)
const countAtom = atom(0) // PrimitiveAtom<number>

// 明示的型定義が必要な場合
const userAtom = atom<User | null>(null)

// Write-only atomの型
const actionAtom = atom<null, [string, number], void>(
  null,
  (get, set, str, num) => { ... }
)

// 型抽出
type CountValue = ExtractAtomValue<typeof countAtom> // number

Testing

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Provider } from 'jotai'
import { useHydrateAtoms } from 'jotai/utils'

// 初期値を注入するヘルパー
function HydrateAtoms({ initialValues, children }) {
  useHydrateAtoms(initialValues)
  return children
}

function TestProvider({ initialValues, children }) {
  return (
    <Provider>
      <HydrateAtoms initialValues={initialValues}>
        {children}
      </HydrateAtoms>
    </Provider>
  )
}

// テスト
test('increments counter', async () => {
  render(
    <TestProvider initialValues={[[countAtom, 5]]}>
      <Counter />
    </TestProvider>
  )

  await userEvent.click(screen.getByRole('button'))
  expect(screen.getByText('6')).toBeInTheDocument()
})

Debugging

// デバッグラベル追加
countAtom.debugLabel = 'count'

// useAtomsDebugValueでProvider内の全atom確認
import { useAtomsDebugValue } from 'jotai-devtools'
function DebugObserver() {
  useAtomsDebugValue()
  return null
}

// Redux DevTools連携
import { useAtomDevtools } from 'jotai-devtools'
useAtomDevtools(countAtom, { name: 'count' })

Best Practices

  1. Atom granularity: Split into small, reusable units
  2. Encapsulation: Hide base atoms and export only derived atoms
  3. Action atom: Separate complex update logic into write-only atoms
  4. Asynchronous processing: Place Suspense and Error Boundary appropriately
  5. atomFamily: As a memory leak countermeasure,remove()orsetShouldRemove()use
  6. TypeScript: Leverage type inference and define types explicitly only when necessary
  7. Testing: Write tests in a form close to user operations

References

For details, refer to the following: