Proxy-state management library that makes state simple for React and vanilla JavaScript applications
—
React hooks optimized for Valtio's proxy system, providing automatic re-rendering only when accessed properties change. The integration offers maximum performance with minimal boilerplate.
Creates a local snapshot that catches changes and triggers re-renders only when accessed parts of the state change. This is the primary hook for reading Valtio state in React components.
/**
* Create a local snapshot that catches changes with render optimization
* Rule of thumb: read from snapshots, mutate the source
* The component will only re-render when accessed parts change
* @param proxyObject - The proxy object to create a snapshot from
* @param options - Configuration options
* @param options.sync - If true, notifications happen synchronously
* @returns A wrapped snapshot in a proxy for render optimization
*/
function useSnapshot<T extends object>(
proxyObject: T,
options?: { sync?: boolean }
): Snapshot<T>;Usage Examples:
import { proxy, useSnapshot } from "valtio";
const state = proxy({
count: 0,
text: "hello",
user: { name: "Alice", age: 25 }
});
// Basic usage - only re-renders when count changes
function Counter() {
const snap = useSnapshot(state);
return (
<div>
{snap.count}
<button onClick={() => ++state.count}>+1</button>
</div>
);
}
// Nested property access - only re-renders when user.name changes
function UserName() {
const snap = useSnapshot(state);
return (
<div>
Hello, {snap.user.name}!
<button onClick={() => state.user.name = "Bob"}>
Change Name
</button>
</div>
);
}
// Partial destructuring - only re-renders when user object changes
function UserProfile() {
const { user } = useSnapshot(state);
return (
<div>
{user.name} ({user.age})
</div>
);
}
// Synchronous updates for immediate re-renders
function ImmediateCounter() {
const snap = useSnapshot(state, { sync: true });
return <div>{snap.count}</div>;
}Important Notes:
useSnapshot call creates a new proxy for render optimizationTakes a proxy and returns a new proxy that can be used safely in both React render functions and event callbacks. The root reference is replaced on every render, but nested keys remain stable.
/**
* Takes a proxy and returns a new proxy for use in both render and callbacks
* The root reference is replaced on every render, but keys below it are stable
* until they're intentionally mutated
* @param proxy - The proxy object to wrap
* @param options - Options passed to useSnapshot internally
* @returns A new proxy for render and callback usage
*/
function useProxy<T extends object>(
proxy: T,
options?: { sync?: boolean }
): T;Usage Examples:
import { proxy, useProxy } from "valtio";
const globalState = proxy({ count: 0, items: [] });
// Export custom hook from your store for better ergonomics
export const useStore = () => useProxy(globalState);
// Component usage
function Counter() {
const store = useStore();
// Can read in render (like snapshot)
const count = store.count;
// Can mutate in callbacks (like original proxy)
const increment = () => { store.count++; };
return (
<div>
{count}
<button onClick={increment}>+1</button>
</div>
);
}
// Direct usage without custom hook
function TodoList() {
const state = useProxy(globalState);
return (
<div>
<button onClick={() => state.items.push(`Item ${state.items.length}`)}>
Add Item
</button>
{state.items.map((item, index) => (
<div key={index}>{item}</div>
))}
</div>
);
}Benefits over useSnapshot:
Important Notes:
useSnapshot internally for render optimizationimport { proxy, useSnapshot } from "valtio";
// Local component state
function TodoApp() {
const [state] = useState(() => proxy({
todos: [],
filter: 'all'
}));
const snap = useSnapshot(state);
const addTodo = (text: string) => {
state.todos.push({ id: Date.now(), text, done: false });
};
return (
<div>
{snap.todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
);
}import { proxy, useSnapshot } from "valtio";
// Global state
export const appState = proxy({
user: null,
theme: 'light',
notifications: []
});
// Multiple components can use the same state
function Header() {
const { user, theme } = useSnapshot(appState);
return (
<header className={theme}>
Welcome, {user?.name}
</header>
);
}
function Settings() {
const { theme } = useSnapshot(appState);
return (
<button onClick={() => appState.theme = theme === 'light' ? 'dark' : 'light'}>
Toggle Theme
</button>
);
}function OptimizedComponent() {
const snap = useSnapshot(state);
// Only accesses count, so only re-renders when count changes
if (snap.count > 10) {
return <div>Count is high: {snap.count}</div>;
}
// If condition is false, text is never accessed
// Component won't re-render when text changes
return <div>Count is low, text: {snap.text}</div>;
}type Snapshot<T> = T extends { $$valtioSnapshot: infer S }
? S
: T extends SnapshotIgnore
? T
: T extends object
? { readonly [K in keyof T]: Snapshot<T[K]> }
: T;
interface Options {
sync?: boolean;
}Install with Tessl CLI
npx tessl i tessl/npm-valtio