Skip to main content
Technology & EngineeringState Management256 lines

Valtio Proxy State Management

Valtio proxy-based state for React — mutable-style API with automatic tracking, snapshots, derived state, and nested object support

Quick Summary22 lines
You are an expert in using Valtio for application state management in React and vanilla JavaScript.

## Key Points

- **Mutable API, immutable under the hood** — write `state.count++` and Valtio handles snapshot diffing
- **Automatic render optimization** — components re-render only when accessed properties change
- **Tiny bundle** — under 3 KB gzipped
- **Framework-agnostic core** — `proxy()` and `snapshot()` work without React
- **Nested object support** — deeply nested mutations are tracked automatically
- **TypeScript-friendly** — full type inference on proxies and snapshots
- **Read from snapshots, write to proxies** — never mutate the snapshot returned by `useSnapshot`; always mutate the original proxy object.
- **Keep actions outside components** — define mutation functions in store files so they are reusable and testable without React.
- **Use `ref()` for non-serializable values** — DOM nodes, WebSocket instances, and class instances should be wrapped in `ref()` to avoid deep proxying.
- **Mutating the snapshot instead of the proxy** — `snap.count++` does nothing useful and may throw in strict mode. Always mutate the proxy: `appState.count++`.

## Quick Example

```bash
npm install valtio
```
skilldb get state-management-skills/Valtio Proxy State ManagementFull skill: 256 lines
Paste into your CLAUDE.md or agent config

Valtio — Proxy-Based State Management

You are an expert in using Valtio for application state management in React and vanilla JavaScript.

Core Philosophy

Overview

Valtio turns plain JavaScript objects into reactive state using ES6 Proxies. You mutate state directly — no reducers, no immutable updates — and Valtio automatically tracks which properties each component reads, re-rendering only when those specific properties change. Under the hood it creates immutable snapshots for React's rendering model while letting you write natural mutable code.

  • Mutable API, immutable under the hood — write state.count++ and Valtio handles snapshot diffing
  • Automatic render optimization — components re-render only when accessed properties change
  • Tiny bundle — under 3 KB gzipped
  • Framework-agnostic coreproxy() and snapshot() work without React
  • Nested object support — deeply nested mutations are tracked automatically
  • TypeScript-friendly — full type inference on proxies and snapshots

Setup & Configuration

npm install valtio
// store/appState.ts
import { proxy } from 'valtio';

interface AppState {
  count: number;
  user: { name: string; email: string } | null;
  todos: { id: string; text: string; done: boolean }[];
}

export const appState = proxy<AppState>({
  count: 0,
  user: null,
  todos: [],
});
// components/Counter.tsx
import { useSnapshot } from 'valtio';
import { appState } from '../store/appState';

export function Counter() {
  const snap = useSnapshot(appState);

  return (
    <div>
      <span>{snap.count}</span>
      <button onClick={() => appState.count++}>+1</button>
    </div>
  );
}

Core Patterns

Direct Mutations

import { appState } from '../store/appState';

// Mutations anywhere — no dispatch, no action creators
export function addTodo(text: string) {
  appState.todos.push({
    id: crypto.randomUUID(),
    text,
    done: false,
  });
}

export function toggleTodo(id: string) {
  const todo = appState.todos.find((t) => t.id === id);
  if (todo) {
    todo.done = !todo.done;
  }
}

export function removeTodo(id: string) {
  const index = appState.todos.findIndex((t) => t.id === id);
  if (index !== -1) {
    appState.todos.splice(index, 1);
  }
}

export function setUser(name: string, email: string) {
  appState.user = { name, email };
}

Snapshots and useSnapshot

import { useSnapshot } from 'valtio';
import { appState } from '../store/appState';

function TodoList() {
  // useSnapshot creates an immutable snapshot and subscribes to changes
  const snap = useSnapshot(appState);

  return (
    <ul>
      {snap.todos.map((todo) => (
        <li key={todo.id}>
          <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
            {todo.text}
          </span>
          {/* Mutate the proxy directly, never the snapshot */}
          <button onClick={() => toggleTodo(todo.id)}>Toggle</button>
        </li>
      ))}
    </ul>
  );
}

Derived State with derive

import { proxy } from 'valtio';
import { derive } from 'valtio/utils';

const cartState = proxy({
  items: [] as { name: string; price: number; qty: number }[],
  taxRate: 0.08,
});

// Derived values update automatically when source state changes
const cartDerived = derive({
  subtotal: (get) =>
    get(cartState).items.reduce((sum, item) => sum + item.price * item.qty, 0),
  tax: (get) => {
    const subtotal = get(cartState).items.reduce(
      (sum, item) => sum + item.price * item.qty,
      0
    );
    return subtotal * get(cartState).taxRate;
  },
  total: (get) => {
    const items = get(cartState).items;
    const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
    return subtotal * (1 + get(cartState).taxRate);
  },
});

subscribeKey for Side Effects

import { subscribe, subscribeKey } from 'valtio';
import { appState } from '../store/appState';

// Subscribe to any change on the proxy
const unsubscribeAll = subscribe(appState, () => {
  console.log('State changed:', appState);
});

// Subscribe to a specific key
const unsubscribeCount = subscribeKey(appState, 'count', (value) => {
  console.log('Count is now:', value);
});

// Cleanup when needed
unsubscribeAll();
unsubscribeCount();

proxyWithHistory for Undo/Redo

import { proxyWithHistory } from 'valtio-history';

const state = proxyWithHistory({
  text: '',
  fontSize: 16,
});

// Mutate as usual
state.value.text = 'Hello';
state.value.fontSize = 20;

// Undo / redo
state.undo();
state.redo();

// Check capabilities
console.log(state.canUndo()); // true
console.log(state.canRedo()); // false after latest change

proxyMap and proxySet

import { proxyMap, proxySet } from 'valtio/utils';

// Reactive Map — standard Map API, fully tracked
const userCache = proxyMap<string, { name: string; email: string }>();

userCache.set('u1', { name: 'Alice', email: 'alice@example.com' });
userCache.delete('u1');

// Reactive Set
const selectedIds = proxySet<string>();
selectedIds.add('item-1');
selectedIds.has('item-1'); // true
selectedIds.delete('item-1');

Nested Proxy References (ref)

import { proxy, ref } from 'valtio';

const state = proxy({
  // ref() opts an object out of deep tracking —
  // useful for large objects you don't want Valtio to proxy deeply
  canvas: ref(document.createElement('canvas')),
  ws: ref(new WebSocket('wss://example.com')),
  config: {
    // normal nested objects are tracked deeply
    theme: 'dark',
    locale: 'en',
  },
});

Best Practices

  • Read from snapshots, write to proxies — never mutate the snapshot returned by useSnapshot; always mutate the original proxy object.
  • Keep actions outside components — define mutation functions in store files so they are reusable and testable without React.
  • Use ref() for non-serializable values — DOM nodes, WebSocket instances, and class instances should be wrapped in ref() to avoid deep proxying.

Common Pitfalls

  • Mutating the snapshot instead of the proxysnap.count++ does nothing useful and may throw in strict mode. Always mutate the proxy: appState.count++.
  • Destructuring the snapshot loses reactivityconst { count } = useSnapshot(state) works, but const snap = useSnapshot(state); const items = snap.list; and then passing items to a child loses tracking. Pass the snapshot or use useSnapshot in the child.

Anti-Patterns

Over-engineering for hypothetical requirements. Building for scenarios that may never materialize adds complexity without value. Solve the problem in front of you first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide wastes time and introduces risk.

Premature abstraction. Creating elaborate frameworks before having enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at system boundaries. Internal code can trust its inputs, but boundaries with external systems require defensive validation.

Skipping documentation. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add state-management-skills

Get CLI access →