| name | jotai-expert |
| description | Jotai状態管理ライブラリのエキスパートスキル。Reactアプリケーションでのatomベースの状態管理を実装する際に使用。以下の場合にこのスキルを使用:
(1) Jotaiのatom設計・実装
(2) 派生atom、非同期atom、atomFamilyの実装
(3) Jotaiのベストプラクティスに基づくリファクタリング
(4) パフォーマンス最適化(selectAtom、splitAtom等)
(5) 永続化(localStorage/sessionStorage連携)
(6) TypeScript型定義
(7) テスト実装
ユーザーが「Jotai」「atom」「状態管理」に関する質問や実装依頼をした場合に発動。
|
Jotai Expert
Jotaiを使用したReact状態管理の実装ガイド。
Core Concepts
Atom
状態の最小単位。値を持たず、Storeに保存される。
const countAtom = atom(0)
const nameAtom = atom('')
const doubleAtom = atom((get) => get(countAtom) * 2)
const countWithLabelAtom = atom(
(get) => `Count: ${get(countAtom)}`,
(get, set, newValue: number) => set(countAtom, newValue)
)
const incrementAtom = atom(null, (get, set) => {
set(countAtom, get(countAtom) + 1)
})
Hooks
const [value, setValue] = useAtom(countAtom)
const value = useAtomValue(countAtom)
const setValue = useSetAtom(countAtom)
Implementation Patterns
Pattern 1: Feature Module
const baseUserAtom = atom<User | null>(null)
export const userAtom = atom((get) => get(baseUserAtom))
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)
const userDataAtom = atom(async (get) => {
const userId = get(userIdAtom)
if (!userId) return null
const response = await fetch(`/api/users/${userId}`)
return response.json()
})
function UserProfile() {
const userData = useAtomValue(userDataAtom)
return <div>{userData?.name}</div>
}
<Suspense fallback={<Loading />}>
<UserProfile />
</Suspense>
Pattern 3: atomFamily
動的にatomを生成・キャッシュ。メモリリーク対策必須。
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
})
Pattern 4: Persistence
import { atomWithStorage } from 'jotai/utils'
const themeAtom = atomWithStorage('theme', 'light')
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()
const derivedAtom = atom(
(get) => get(formAtom),
(get, set, newValue) => {
set(formAtom, newValue === RESET ? RESET : newValue)
}
)
Performance Optimization
selectAtom
大きなオブジェクトから一部のみ取得。派生atomを優先し、必要な場合のみ使用。
import { selectAtom } from 'jotai/utils'
const personAtom = atom({ name: 'John', age: 30, address: {...} })
const nameAtom = selectAtom(personAtom, (person) => person.name)
const stableNameAtom = useMemo(
() => selectAtom(personAtom, (p) => p.name),
[]
)
splitAtom
配列の各要素を独立した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)
const userAtom = atom<User | null>(null)
const actionAtom = atom<null, [string, number], void>(
null,
(get, set, str, num) => { ... }
)
type CountValue = ExtractAtomValue<typeof countAtom>
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'
import { useAtomsDebugValue } from 'jotai-devtools'
function DebugObserver() {
useAtomsDebugValue()
return null
}
import { useAtomDevtools } from 'jotai-devtools'
useAtomDevtools(countAtom, { name: 'count' })
Best Practices
- Atom粒度: 小さく再利用可能な単位に分割
- カプセル化: base atomを隠蔽し、派生atomのみをexport
- Action atom: 複雑な更新ロジックはwrite-only atomに分離
- 非同期処理: SuspenseとError Boundaryを適切に配置
- atomFamily: メモリリーク対策として
remove()またはsetShouldRemove()を使用
- TypeScript: 型推論を活用し、必要な場合のみ明示的に型定義
- テスト: ユーザー操作に近い形でテストを記述
References
詳細は以下を参照: