tanstack-store について
TanStack Storeは、TanStackライブラリや対応するフレームワークアダプターを使う開発者向けのリアクティブな状態管理ライブラリです。ストア、計算値、エフェクト、バッチ更新を備えています。
主な機能
- 01リアクティブな状態ストア
- 02Derivedによる計算値
- 03クリーンアップ対応のエフェクト
- 04バッチ更新
- 05フレームワークアダプターとReact hooks
活用シーン
- 01アプリケーションの共有状態管理
- 02フィルターやソート済みデータの導出
- 03状態変化に応じた副作用の実行
- 04関連する更新の通知をまとめる
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ tanstack-skills/ tanstack-skills --skill tanstack-storeインストール後、Claude Code で/tanstack-storeを入力して読み込まれたか確認します。
詳細
概要
TanStack Store は、TanStack ライブラリの内部機能を支える軽量なリアクティブストア(シグナルに似たストア)です。状態を管理する Store、計算値を管理する Derived、副作用を管理する Effect、アトミックな更新を行う batch を提供します。フレームワークアダプターはリアクティブなフックを提供します。
コア: @tanstack/store
React: @tanstack/react-store
インストール
npm install @tanstack/store @tanstack/react-store
Store
Store の作成
import { Store } from '@tanstack/store'
const countStore = new Store(0)
const userStore = new Store<{ name: string; email: string }>({
name: 'Alice',
email: '[email protected]',
})
状態の更新
// Function updater (immutable update)
countStore.setState((prev) => prev + 1)
userStore.setState((prev) => ({ ...prev, name: 'Bob' }))
変更の購読
const unsub = countStore.subscribe(() => {
console.log('Count:', countStore.state)
})
// Cleanup
unsub()
Store のオプション
const store = new Store(initialState, {
// Custom update function
updateFn: (prevValue) => (updater) => {
return updater(prevValue) // custom logic
},
// Callback on subscribe
onSubscribe: (listener, store) => {
console.log('New subscriber')
return () => console.log('Unsubscribed')
},
// Callback on every update
onUpdate: () => {
console.log('State updated:', store.state)
},
})
Store のプロパティ
store.state // Current state
store.prevState // Previous state
store.listeners // Set of listener callbacks
Derived(計算値)
import { Store, Derived } from '@tanstack/store'
const count = new Store(5)
const multiplier = new Store(2)
const doubled = new Derived({
deps: [count, multiplier],
fn: ({ currDepVals }) => currDepVals[0] * currDepVals[1],
})
// MUST mount to activate
const unmount = doubled.mount()
console.log(doubled.state) // 10
count.setState(() => 10)
console.log(doubled.state) // 20
// Cleanup
unmount()
前の値を使った Derived
const accumulated = new Derived({
deps: [count],
fn: ({ prevVal, currDepVals }) => {
return currDepVals[0] + (prevVal ?? 0)
},
})
Derived の連結
const filtered = new Derived({
deps: [dataStore, filterStore],
fn: ({ currDepVals }) => currDepVals[0].filter(matchesFilter(currDepVals[1])),
})
const sorted = new Derived({
deps: [filtered, sortStore],
fn: ({ currDepVals }) => [...currDepVals[0]].sort(comparator(currDepVals[1])),
})
const paginated = new Derived({
deps: [sorted, pageStore],
fn: ({ currDepVals }) => currDepVals[0].slice(
currDepVals[1].offset,
currDepVals[1].offset + currDepVals[1].limit,
),
})
Effect(副作用)
import { Store, Effect } from '@tanstack/store'
const count = new Store(0)
const logger = new Effect({
deps: [count],
fn: () => {
console.log('Count changed:', count.state)
// Optionally return cleanup function
return () => console.log('Cleaning up')
},
eager: false, // true = run immediately on mount
})
const unmount = logger.mount()
count.setState(() => 1) // logs: "Count changed: 1"
unmount()
クリーンアップを伴う Effect
const timerEffect = new Effect({
deps: [intervalStore],
fn: () => {
const id = setInterval(() => { /* ... */ }, intervalStore.state)
return () => clearInterval(id) // cleanup on next run or unmount
},
})
Batch
複数の更新をまとめて 1 回の通知にします。
import { batch } from '@tanstack/store'
// Subscribers fire only once with final state
batch(() => {
countStore.setState(() => 1)
nameStore.setState(() => 'Alice')
settingsStore.setState((prev) => ({ ...prev, theme: 'dark' }))
})
React との統合
useStore フック
import { useStore } from '@tanstack/react-store'
// Subscribe to full state
function Counter() {
const count = useStore(countStore)
return <button onClick={() => countStore.setState((c) => c + 1)}>{count}</button>
}
// Subscribe with selector (performance optimization)
function UserName() {
const name = useStore(userStore, (state) => state.name)
return <span>{name}</span>
}
// Subscribe to Derived
function DoubledDisplay() {
const value = useStore(doubledDerived)
return <span>{value}</span>
}
shallow 等価性関数
セレクターが構造的に等しいオブジェクトを返す場合、再レンダリングを防ぎます。
import { useStore } from '@tanstack/react-store'
import { shallow } from '@tanstack/react-store'
function TodoList() {
// Without shallow: re-renders on ANY state change (new object ref)
// With shallow: only re-renders when items actually change
const items = useStore(todosStore, (state) => state.items, shallow)
return <ul>{items.map(/* ... */)}</ul>
}
React での Derived/Effect のマウント
function MyComponent() {
useEffect(() => {
const unmountDerived = myDerived.mount()
const unmountEffect = myEffect.mount()
return () => {
unmountDerived()
unmountEffect()
}
}, [])
const value = useStore(myDerived)
return <span>{value}</span>
}
モジュールレベルの Store パターン
// stores/counter.ts
import { Store, Derived } from '@tanstack/store'
export const counterStore = new Store(0)
export const doubledCount = new Derived({
deps: [counterStore],
fn: ({ currDepVals }) => currDepVals[0] * 2,
})
// Actions as plain functions
export function increment() {
counterStore.setState((c) => c + 1)
}
export function reset() {
counterStore.setState(() => 0)
}
フレームワークアダプター
| フレームワーク | パッケージ | フック/コンポーザブル |
|---|---|---|
| React | @tanstack/react-store | useStore(store, selector?, equalityFn?) |
| Vue | @tanstack/vue-store | useStore(store, selector?)(computed ref を返します) |
| Solid | @tanstack/solid-store | useStore(store, selector?)(シグナルを返します) |
| Angular | @tanstack/angular-store | injectStore(store, selector?)(シグナルを返します) |
| Svelte | @tanstack/svelte-store | useStore(store, selector?)($state を返します) |
ベストプラクティス
- ストアはモジュールレベルで定義します - シングルトンです
useStoreではセレクターを使います。不要な再レンダリングを防げます- セレクターがオブジェクト/配列を返す場合は
shallowを使います - Derived と Effect のインスタンスでは、必ず
mount()を呼び出します - アンマウント関数は必ずクリーンアップします(特に React の useEffect 内)
- 状態を直接変更しないでください - 必ず
setStateを使います - 関連する複数の更新には
batchを使います - データ変換には Derived の連鎖を使います(フィルター -> ソート -> ページネーション)
- タイマー/リスナーには、Effect の
fnからクリーンアップ関数を返します - 可能な場合はプリミティブ値を選択します(等価性関数は不要です)
よくある落とし穴
- Derived/Effect の
mount()を忘れる(有効になりません) - サブスクリプション/アンマウント関数をクリーンアップしない(メモリーリークにつながります)
setStateを使わずにstore.stateを直接変更するshallowを使わずに、セレクターで新しいオブジェクト参照を作成する- セレクターを指定せずに
useStoreを使う(すべてを購読します) - Effect をすぐに実行する必要があるのに
eager: trueを忘れる
コメント
Markdown 対応