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.
Manages reactive state using Valtio's proxy-based system for mutable state syntax and fine-grained reactivity. Use when you need state management outside React components or want direct mutation syntax instead of setState patterns.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/patterns.mdreferences/utils.mdProxy-based state management that makes React state feel like plain JavaScript.
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>
);
}
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' });
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 });
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;
// 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;
},
};
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'
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 }
);
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();
import { subscribeKey } from 'valtio/utils';
subscribeKey(store, 'count', (value) => {
console.log('Count is now:', value);
document.title = `Count: ${value}`;
});
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();
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()),
});
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
}
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');
import { devtools } from 'valtio/utils';
// Connect to Redux DevTools
const unsub = devtools(store, {
name: 'MyApp Store',
enabled: process.env.NODE_ENV === 'development',
});
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();
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++;
};
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();
});
});
// 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';
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
},
});
| 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 |
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.