Valtio Proxy State Management
Valtio proxy-based state for React — mutable-style API with automatic tracking, snapshots, derived state, and nested object support
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 linesValtio — 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 core —
proxy()andsnapshot()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 inref()to avoid deep proxying.
Common Pitfalls
- Mutating the snapshot instead of the proxy —
snap.count++does nothing useful and may throw in strict mode. Always mutate the proxy:appState.count++. - Destructuring the snapshot loses reactivity —
const { count } = useSnapshot(state)works, butconst snap = useSnapshot(state); const items = snap.list;and then passingitemsto a child loses tracking. Pass the snapshot or useuseSnapshotin 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
Related Skills
Jotai Atomic State Management
Jotai atomic state for React — primitive/derived/async atoms, Provider-less mode, atomWithStorage, atomWithQuery, and React Suspense integration
Legend State
Legend-State high-performance observable state for React — fine-grained reactivity, persistence plugins, computed observables, and sync engine
Nanostores
Nanostores tiny framework-agnostic state manager — atoms, computed stores, lifecycle events, and integrations with React, Vue, Svelte, and vanilla JS
Redux Toolkit
Redux Toolkit for scalable React state — createSlice, configureStore, RTK Query, createAsyncThunk, entity adapter, middleware, and TypeScript patterns
TanStack Query (React Query)
TanStack Query for server state — useQuery, useMutation, query invalidation, optimistic updates, infinite queries, prefetching, and SSR hydration
XState State Machines
XState for state machines and statecharts in React — actors, guards, actions, services, @xstate/react integration, and the visual editor