Skip to main content

Valtio Adapter

Track Valtio proxy state changes with React Dev Debugger for better debugging.

Installation

npm install react-dev-debugger valtio

Basic Setup

store.ts
import { proxy } from 'valtio';
import { createValtioAdapter } from 'react-dev-debugger/adapters/valtio';

// Create your proxy state
export const state = proxy({
count: 0,
user: null,
todos: [],
});

// Connect debugger
createValtioAdapter(state, {
name: 'App State',
});

Live Playground

Counter Store

Count: 0

User Store

Age: 25

Todos Store

    Total: 0Completed: 0

    Proxy States

    Valtio Adapter Configuration

    import { proxy } from 'valtio';
    import { createValtioAdapter } from 'react-dev-debugger/adapters/valtio';
    
    // Create proxy stores
    const counterStore = proxy({
      count: 0,
      increment: () => counterStore.count++,
      decrement: () => counterStore.count--,
    });
    
    const userStore = proxy({
      name: 'John',
      email: 'john@example.com',
      updateName: (name) => userStore.name = name,
    });
    
    // Create adapter
    const adapter = createValtioAdapter({
      name: 'My Store',
      proxies: {
        counter: counterStore,
        user: userStore,
      },
    });
    
    // Subscribe to changes
    adapter.subscribe((state) => {
      console.log('Proxy states:', state.proxies);
    });

    Usage in Components

    Counter.tsx
    import { useSnapshot } from 'valtio';
    import { state } from './store';

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

    return (
    <div>
    <h2>Count: {snap.count}</h2>
    <button onClick={() => state.count++}>Increment</button>
    <button onClick={() => state.count--}>Decrement</button>
    </div>
    );
    }

    Nested State

    Valtio handles nested objects automatically:

    store.ts
    import { proxy } from 'valtio';
    import { createValtioAdapter } from 'react-dev-debugger/adapters/valtio';

    export const state = proxy({
    user: {
    name: '',
    email: '',
    preferences: {
    theme: 'dark',
    notifications: true,
    },
    },
    cart: {
    items: [],
    total: 0,
    },
    });

    createValtioAdapter(state, {
    name: 'App Store',
    });
    UserProfile.tsx
    import { useSnapshot } from 'valtio';
    import { state } from './store';

    export function UserProfile() {
    const snap = useSnapshot(state);

    const updateName = (name: string) => {
    state.user.name = name;
    };

    const toggleTheme = () => {
    state.user.preferences.theme =
    snap.user.preferences.theme === 'dark' ? 'light' : 'dark';
    };

    return (
    <div>
    <input
    value={snap.user.name}
    onChange={(e) => updateName(e.target.value)}
    />
    <button onClick={toggleTheme}>
    Theme: {snap.user.preferences.theme}
    </button>
    </div>
    );
    }

    Actions Pattern

    Organize state mutations with actions:

    store.ts
    import { proxy } from 'valtio';
    import { createValtioAdapter } from 'react-dev-debugger/adapters/valtio';

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

    export const state = proxy<{
    todos: Todo[];
    }>({
    todos: [],
    });

    // Actions
    export const actions = {
    addTodo(text: string) {
    state.todos.push({
    id: Date.now(),
    text,
    completed: false,
    });
    },

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

    deleteTodo(id: number) {
    const index = state.todos.findIndex(t => t.id === id);
    if (index !== -1) {
    state.todos.splice(index, 1);
    }
    },

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

    createValtioAdapter(state, {
    name: 'Todo Store',
    });
    TodoList.tsx
    import { useState } from 'react';
    import { useSnapshot } from 'valtio';
    import { state, actions } from './store';

    export function TodoList() {
    const snap = useSnapshot(state);
    const [input, setInput] = useState('');

    const handleAdd = () => {
    if (input.trim()) {
    actions.addTodo(input);
    setInput('');
    }
    };

    return (
    <div>
    <h1>Valtio Todo List</h1>

    <div>
    <input
    value={input}
    onChange={(e) => setInput(e.target.value)}
    onKeyPress={(e) => e.key === 'Enter' && handleAdd()}
    />
    <button onClick={handleAdd}>Add</button>
    </div>

    <ul>
    {snap.todos.map((todo) => (
    <li key={todo.id}>
    <input
    type="checkbox"
    checked={todo.completed}
    onChange={() => actions.toggleTodo(todo.id)}
    />
    <span style={{
    textDecoration: todo.completed ? 'line-through' : 'none'
    }}>
    {todo.text}
    </span>
    <button onClick={() => actions.deleteTodo(todo.id)}>
    Delete
    </button>
    </li>
    ))}
    </ul>

    {snap.todos.some(t => t.completed) && (
    <button onClick={actions.clearCompleted}>
    Clear Completed
    </button>
    )}
    </div>
    );
    }

    Computed Values

    Use derive for computed values:

    store.ts
    import { proxy } from 'valtio';
    import { derive } from 'valtio/utils';
    import { createValtioAdapter } from 'react-dev-debugger/adapters/valtio';

    export const state = proxy({
    items: [],
    filter: 'all',
    });

    export const derived = derive({
    filteredItems: (get) => {
    const items = get(state).items;
    const filter = get(state).filter;

    if (filter === 'active') {
    return items.filter(item => !item.completed);
    }
    if (filter === 'completed') {
    return items.filter(item => item.completed);
    }
    return items;
    },
    stats: (get) => {
    const items = get(state).items;
    return {
    total: items.length,
    completed: items.filter(i => i.completed).length,
    active: items.filter(i => !i.completed).length,
    };
    },
    });

    createValtioAdapter(state, { name: 'State' });
    createValtioAdapter(derived, { name: 'Derived' });

    Subscriptions

    Track specific state changes:

    import { subscribe } from 'valtio';
    import { state } from './store';

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

    // Cleanup
    unsubscribe();

    Configuration Options

    interface ValtioAdapterOptions {
    // Display name
    name?: string;

    // Maximum snapshots to keep
    maxSnapshots?: number;

    // Transform state before displaying
    stateTransform?: (state: any) => any;
    }

    Debugging Tips

    Mutation Tracking

    Every direct mutation to the proxy is tracked and appears in the timeline.

    Nested Updates

    The debugger shows the exact path that changed (e.g., user.preferences.theme).

    Performance

    Valtio's proxy-based approach means minimal overhead for tracking.

    Best Practices

    ✅ Do's

    • Use actions to organize mutations
    • Keep state flat when possible
    • Use derive for computed values
    • Leverage snapshots for reading

    ❌ Don'ts

    • Don't mutate snapshots
    • Avoid deep nesting (impacts performance)
    • Don't mix proxy and non-proxy objects

    Next Steps