valtio

star 4

Manages state with Valtio using proxy-based reactivity, direct mutations, and automatic re-renders. Use when wanting mutable state syntax, fine-grained reactivity, or state management outside React components.

mgd34msu By mgd34msu schedule Updated 1/15/2026

name: valtio description: Manages state with Valtio using proxy-based reactivity, direct mutations, and automatic re-renders. Use when wanting mutable state syntax, fine-grained reactivity, or state management outside React components.

Valtio

Proxy-based state management that makes React state feel like plain JavaScript.

Quick Start

Install:

npm install valtio

Create state:

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

interface Store {
  count: number;
  users: User[];
  filter: 'all' | 'active' | 'completed';
}

export const store = proxy<Store>({
  count: 0,
  users: [],
  filter: 'all',
});

// Actions - mutate directly
export const increment = () => {
  store.count++;
};

export const addUser = (user: User) => {
  store.users.push(user);
};

Use in React:

import { useSnapshot } from 'valtio';
import { store, increment } from './store';

function Counter() {
  // Only re-renders when count changes
  const snap = useSnapshot(store);

  return (
    <div>
      <p>Count: {snap.count}</p>
      <button onClick={increment}>+1</button>
      {/* Or mutate directly */}
      <button onClick={() => store.count++}>+1</button>
    </div>
  );
}

Core Concepts

proxy() - Create Reactive State

import { proxy } from 'valtio';

// Simple state
const state = proxy({ count: 0, text: '' });

// Nested objects are automatically proxied
const store = proxy({
  user: {
    name: 'John',
    settings: {
      theme: 'dark',
      notifications: true,
    },
  },
  todos: [],
});

// Mutations work at any depth
store.user.settings.theme = 'light';
store.todos.push({ id: 1, text: 'Learn Valtio' });

useSnapshot() - Read in React

Returns a frozen, read-only snapshot that triggers re-renders only when accessed properties change.

import { useSnapshot } from 'valtio';

function UserProfile() {
  const snap = useSnapshot(store);

  // Only re-renders when user.name changes
  return <p>{snap.user.name}</p>;
}

function TodoList() {
  const snap = useSnapshot(store);

  // Only re-renders when todos array changes
  return (
    <ul>
      {snap.todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

Synchronous updates:

// For immediate renders (useful in tests)
const snap = useSnapshot(store, { sync: true });

Mutations - Write to Proxy

Always mutate the proxy, never the snapshot.

// Direct mutations
store.count++;
store.user.name = 'Jane';

// Array mutations
store.items.push(newItem);
store.items.splice(index, 1);
store.items[0].done = true;

// Object replacement
store.user = { ...store.user, name: 'Jane' };

// Delete properties
delete store.user.email;

Actions Pattern

// store/todos.ts
import { proxy } from 'valtio';

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

interface TodoStore {
  todos: Todo[];
  filter: 'all' | 'active' | 'completed';
}

export const todoStore = proxy<TodoStore>({
  todos: [],
  filter: 'all',
});

// Actions - define alongside store
export const actions = {
  addTodo(text: string) {
    todoStore.todos.push({
      id: crypto.randomUUID(),
      text,
      completed: false,
    });
  },

  toggleTodo(id: string) {
    const todo = todoStore.todos.find(t => t.id === id);
    if (todo) {
      todo.completed = !todo.completed;
    }
  },

  removeTodo(id: string) {
    const index = todoStore.todos.findIndex(t => t.id === id);
    if (index >= 0) {
      todoStore.todos.splice(index, 1);
    }
  },

  clearCompleted() {
    todoStore.todos = todoStore.todos.filter(t => !t.completed);
  },

  setFilter(filter: TodoStore['filter']) {
    todoStore.filter = filter;
  },
};

Computed Properties

Getters

const store = proxy({
  firstName: 'John',
  lastName: 'Doe',

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },

  todos: [] as Todo[],

  get completedCount() {
    return this.todos.filter(t => t.completed).length;
  },

  get activeCount() {
    return this.todos.length - this.completedCount;
  },
});

// Usage
console.log(store.fullName); // 'John Doe'

derive() - Cross-Store Computations

import { proxy } from 'valtio';
import { derive } from 'derive-valtio';

const userStore = proxy({ name: 'John', role: 'admin' });
const settingsStore = proxy({ theme: 'dark' });

// Create derived state from multiple stores
const derived = derive({
  greeting: (get) => `Hello, ${get(userStore).name}!`,
  canEdit: (get) => get(userStore).role === 'admin',
});

// Attach derived properties to existing proxy
derive(
  {
    isDark: (get) => get(settingsStore).theme === 'dark',
  },
  { proxy: userStore }
);

Subscriptions

subscribe() - React to Changes

import { subscribe } from 'valtio';

// Subscribe to all changes
const unsubscribe = subscribe(store, () => {
  console.log('Store changed:', store);
});

// Subscribe to nested object
subscribe(store.user, () => {
  console.log('User changed');
});

// Persist to localStorage
subscribe(store, () => {
  localStorage.setItem('store', JSON.stringify(store));
});

// Cleanup
unsubscribe();

subscribeKey() - Watch Single Property

import { subscribeKey } from 'valtio/utils';

subscribeKey(store, 'count', (value) => {
  console.log('Count is now:', value);
  document.title = `Count: ${value}`;
});

watch() - Auto-tracking

import { watch } from 'valtio/utils';

const stop = watch((get) => {
  // Automatically subscribes to accessed properties
  console.log('User:', get(store).user.name);
  console.log('Count:', get(store).count);
});

// Later
stop();

Utilities

ref() - Escape Proxy

Wrap values that shouldn't be proxied (DOM nodes, class instances, large data).

import { proxy, ref } from 'valtio';

const store = proxy({
  // DOM node - don't proxy
  canvas: ref(document.createElement('canvas')),

  // Large dataset - don't proxy for performance
  bigData: ref(hugeArray),

  // Class instance - preserve prototype
  date: ref(new Date()),
});

snapshot() - Get Immutable Copy

import { snapshot } from 'valtio';

const snap = snapshot(store);

// Useful for:
// - Sending to API
// - Logging
// - Comparison
console.log(JSON.stringify(snap));

// Deep equality check
if (snapshot(store) !== previousSnap) {
  // State changed
}

proxySet() and proxyMap()

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

// Reactive Set
const selectedIds = proxySet<string>(['id1', 'id2']);
selectedIds.add('id3');
selectedIds.delete('id1');
selectedIds.has('id2'); // true

// Reactive Map
const users = proxyMap<string, User>([
  ['user1', { name: 'John' }],
]);
users.set('user2', { name: 'Jane' });
users.get('user1'); // { name: 'John' }
users.delete('user1');

devtools()

import { devtools } from 'valtio/utils';

// Connect to Redux DevTools
const unsub = devtools(store, {
  name: 'MyApp Store',
  enabled: process.env.NODE_ENV === 'development',
});

Async Actions

const store = proxy({
  users: [] as User[],
  loading: false,
  error: null as string | null,
});

export async function fetchUsers() {
  store.loading = true;
  store.error = null;

  try {
    const response = await fetch('/api/users');
    store.users = await response.json();
  } catch (e) {
    store.error = e instanceof Error ? e.message : 'Unknown error';
  } finally {
    store.loading = false;
  }
}

// Can call from anywhere - not just React
fetchUsers();

Outside React

Valtio works without React.

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

const state = proxy({ count: 0 });

// Subscribe to changes
subscribe(state, () => {
  const snap = snapshot(state);
  document.getElementById('count')!.textContent = String(snap.count);
});

// Update from anywhere
document.getElementById('btn')!.onclick = () => {
  state.count++;
};

Testing

import { proxy, snapshot } from 'valtio';
import { store, actions } from './store';

describe('todoStore', () => {
  beforeEach(() => {
    // Reset state
    store.todos = [];
    store.filter = 'all';
  });

  it('adds todo', () => {
    actions.addTodo('Test todo');

    expect(store.todos).toHaveLength(1);
    expect(store.todos[0].text).toBe('Test todo');
  });

  it('toggles todo', () => {
    actions.addTodo('Test');
    const id = store.todos[0].id;

    actions.toggleTodo(id);

    expect(store.todos[0].completed).toBe(true);
  });

  it('creates immutable snapshot', () => {
    store.count = 5;
    const snap = snapshot(store);

    expect(snap.count).toBe(5);
    expect(() => {
      (snap as any).count = 10;
    }).toThrow();
  });
});

Common Patterns

Store Slices

// stores/user.ts
export const userStore = proxy({
  user: null as User | null,
  login(user: User) {
    this.user = user;
  },
  logout() {
    this.user = null;
  },
});

// stores/cart.ts
export const cartStore = proxy({
  items: [] as CartItem[],
  add(item: CartItem) {
    this.items.push(item);
  },
});

// stores/index.ts - combine if needed
export { userStore } from './user';
export { cartStore } from './cart';

Form State

const formStore = proxy({
  values: {
    email: '',
    password: '',
  },
  errors: {} as Record<string, string>,
  touched: {} as Record<string, boolean>,

  setField(field: string, value: string) {
    this.values[field] = value;
    this.touched[field] = true;
    this.validate(field);
  },

  validate(field?: string) {
    // Validation logic
  },
});

Best Practices

  1. Read from snapshot, write to proxy - Never mutate snap
  2. Define actions as functions - Keep mutations organized
  3. Use ref() for non-reactive data - DOM nodes, large arrays
  4. Keep stores small - Split by domain/feature
  5. TypeScript interfaces - Define types for store shape

Common Mistakes

Mistake Fix
Mutating snapshot Mutate proxy instead
Spreading proxy in render Use snapshot values
Large objects without ref() Wrap with ref()
Async in render Move to action function

Reference Files

Install via CLI
npx skills add https://github.com/mgd34msu/goodvibes-gemini --skill valtio
Repository Details
star Stars 4
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator