valtio の使い方

🧙 Valtio makes proxy-state simple for React and Vanilla

v2.3.1/週MIT状態管理
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

valtio の使い方 — Proxyベースの直感的なReact状態管理

一言でいうと

valtioは、JavaScriptのProxyを活用して普通のオブジェクト操作だけで状態管理を実現するライブラリです。ミュータブルな書き味でありながら、Reactコンポーネントのレンダリング最適化を自動で行ってくれます。

どんな時に使う?

  • Reduxのボイラープレートに疲れた時 — action/reducer/dispatchなしに、オブジェクトを直接変更するだけで状態管理したい場合
  • コンポーネント外からも状態を操作したい時 — WebSocket受信ハンドラやsetIntervalなど、React外のロジックから自然に状態を更新したい場合
  • レンダリング最適化を自動でやりたい時 — アクセスしたプロパティだけを追跡し、不要な再レンダリングを自動で防ぎたい場合

インストール

# npm
npm install valtio

# yarn
yarn add valtio

# pnpm
pnpm add valtio

valtio の基本的な使い方

valtioの基本は3ステップです。proxyで状態を作り、直接変更し、useSnapshotで読み取る。これだけです。

import { proxy, useSnapshot } from 'valtio'

// 1. 状態を作る
const state = proxy({
  count: 0,
  text: 'hello',
})

// 2. どこからでも変更できる(React外でもOK)
const increment = () => {
  ++state.count
}

// 3. コンポーネントでスナップショットを読む
function Counter() {
  const snap = useSnapshot(state)
  // snap.count の変更時のみ再レンダリング(snap.text の変更では再レンダリングしない)
  return (
    <div>
      <p>Count: {snap.count}</p>
      <button onClick={increment}>+1</button>
    </div>
  )
}

重要な原則: レンダリング(JSX内)では snap から読み取り、変更は state に対して行います。

よく使うAPI

1. proxy — 状態オブジェクトの作成

import { proxy } from 'valtio'

interface AppState {
  user: { name: string; age: number } | null
  todos: { id: number; title: string; done: boolean }[]
  loading: boolean
}

const appState = proxy<AppState>({
  user: null,
  todos: [],
  loading: false,
})

// ネストしたオブジェクトも自動的にProxyになる
appState.user = { name: 'Taro', age: 30 }
appState.todos.push({ id: 1, title: 'Buy milk', done: false })

2. useSnapshot — Reactコンポーネントでの状態読み取り

import { useSnapshot } from 'valtio'

function TodoList() {
  const snap = useSnapshot(appState)

  return (
    <ul>
      {snap.todos.map((todo) => (
        <li key={todo.id}>
          <span
            style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
          >
            {todo.title}
          </span>
          <button
            onClick={() => {
              // 変更は元の state に対して行う
              const target = appState.todos.find((t) => t.id === todo.id)
              if (target) target.done = !target.done
            }}
          >
            Toggle
          </button>
        </li>
      ))}
    </ul>
  )
}

// sync オプション: input要素のようにバッチングを無効にしたい場合
function TextInput() {
  const snap = useSnapshot(appState, { sync: true })
  return (
    <input
      value={snap.user?.name ?? ''}
      onChange={(e) => {
        if (appState.user) appState.user.name = e.target.value
      }}
    />
  )
}

3. subscribe — 状態変更の購読

import { subscribe } from 'valtio'
import { subscribeKey } from 'valtio/utils'

// 状態全体の変更を購読
const unsubscribe = subscribe(appState, () => {
  console.log('State changed:', appState)
})

// ネストしたオブジェクトだけを購読
subscribe(appState.todos, () => {
  console.log('Todos changed:', appState.todos)
})

// 特定のキーだけを購読(プリミティブ値に便利)
subscribeKey(appState, 'loading', (value) => {
  console.log('Loading changed to:', value)
})

// 購読解除
unsubscribe()

4. ref — Proxy化を避けるオブジェクトの保持

DOM要素やサードパーティのインスタンスなど、Proxyで包みたくないオブジェクトに使います。

import { proxy, ref } from 'valtio'

const editorState = proxy({
  content: '',
  // DOM要素やクラスインスタンスはrefで包む
  canvasElement: ref<HTMLCanvasElement | null>(null),
  // 大きなオブジェクトでトラッキング不要なものにも有効
  cachedData: ref({ huge: 'object', that: 'should not be proxied' }),
})

5. watch / devtools — ユーティリティ

import { watch, devtools } from 'valtio/utils'

// watch: 使用した状態を自動購読(computed的な用途に)
const stop = watch((get) => {
  // get() でアクセスした proxy を自動的に購読する
  const { count } = get(appState)
  console.log('Count is now:', count)
})

// 不要になったら停止
stop()

// devtools: Redux DevTools Extension との連携
const unsubDevtools = devtools(appState, {
  name: 'App State',
  enabled: process.env.NODE_ENV === 'development',
})

類似パッケージとの比較

特徴valtioZustandJotaiMobX
パラダイムProxy (mutable風)Flux (immutable)AtomicProxy (mutable)
バンドルサイズ~3KB~1KB~3KB~16KB
学習コスト低い低い中程度中〜高
レンダリング最適化自動(プロパティ追跡)セレクタで手動原子単位で自動自動
React外での利用○ (valtio/vanilla)
ボイラープレート最小少ない少ないやや多い
TypeScript良好良好良好良好
開発元pmndrspmndrspmndrsMichel Weststrate

Zustand・Jotai・valtioは同じpmndrsコミュニティが開発しています。ミュータブルな書き味が好みならvaltio、シンプルなストアならZustand、原子的な状態管理ならJotaiが適しています。

注意点・Tips

⚠️ snap に書き込まない

useSnapshot が返すオブジェクトはフリーズされた読み取り専用です。変更は必ず元の proxy オブジェクトに対して行ってください。

// ❌ NG
const snap = useSnapshot(state)
snap.count++ // エラーまたは無視される

// ✅ OK
state.count++

⚠️ this の使用を避ける

オブジェクト内のメソッドで this を使うと、スナップショット経由で呼び出した際に問題が起きます。アロー関数で直接 state を参照するのが安全です。

// ❌ NG: thisはsnap経由だとフリーズされたオブジェクトを指す
const state = proxy({
  count: 0,
  inc() { ++this.count },
})

// ✅ OK: アロー関数でstateを直接参照
const state = proxy({
  count: 0,
  inc: () => { ++state.count },
})

⚠️ TypeScriptでスナップショットの型が厳しすぎる場合

useSnapshot の戻り値はDeepReadonly型になります。既存の型と合わない場合は型定義を緩和できます。

// 必要に応じてモジュール拡張で型を緩和
declare module 'valtio' {
  function useSnapshot<T extends object>(p: T): T
}

💡 ESLintプラグインの導入を推奨

snapstate の使い分けミスを防ぐために、eslint-plugin-valtio の導入を強くおすすめします。

npm install -D eslint-plugin-valtio

💡 Vanilla JS でも使える

Reactに依存しない valtio/vanilla からインポートすれば、Node.jsやVanilla JSプロジェクトでも利用可能です。

import { proxy, subscribe, snapshot } from 'valtio/vanilla'

💡 <input> には sync: true を使う

デフォルトではstate変更はバッチ処理されるため、テキスト入力で文字が飛ぶ問題が起きることがあります。useSnapshot(state, { sync: true }) で解決できます。

まとめ

valtioは「普通のJavaScriptオブジェクトを変更するだけ」という最もシンプルなメンタルモデルで状態管理を実現するライブラリです。Proxyによる自動的なレンダリング最適化と、React外からでも自由に状態を操作できる柔軟性が大きな魅力です。ボイラープレートを最小限に抑えたい方や、MobXのようなリアクティブな書き味をより軽量に実現したい方に最適な選択肢と言えるでしょう。