executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
valtioValtio is a proxy-based state library for React: wrap an object in proxy(), mutate it like plain JavaScript, and components that read it through useSnapshot re-render only for the properties they used. Use when someone asks for "simple React state management", "Valtio", "proxy state", "mutable state in React", "alternative to Zustand/Redux", or "state manag…
Independent GitHub snapshot · Automated classification · Original documentation governs
Valtio makes React state management feel like plain JavaScript — mutate objects directly and React re-renders automatically. No reducers, no actions, no selectors. Wrap an object in proxy(), mutate it anywhere, and components that read the changed properties re-render. Based on JavaScript Proxy, it tracks which properties each component uses and only re-renders when those specific properties change.
This skill targets Valtio 2 (current release 2.3.x). Compared with v1: promises stored in a proxy are no longer resolved for you, proxy(obj) wraps the object you pass instead of copying it, and derive is no longer part of valtio/utils.
set() functionnpm install valtio
// store/app.ts — Define state as a plain object
import { proxy } from "valtio";
export const appState = proxy({
user: null as { name: string; email: string } | null,
theme: "light" as "light" | "dark",
notifications: [] as Array<{ id: string; text: string; read: boolean }>,
sidebar: { open: true, width: 280 },
// Computed value: an object getter that reads sibling properties through `this`
get unreadCount() {
return this.notifications.filter((n) => !n.read).length;
},
});
// Mutate directly — React components auto-update
export function login(user: { name: string; email: string }) {
appState.user = user;
}
export function toggleTheme() {
appState.theme = appState.theme === "light" ? "dark" : "light";
}
export function addNotification(text: string) {
appState.notifications.push({ id: crypto.randomUUID(), text, read: false });
}
export function markAllRead() {
appState.notifications.forEach((n) => { n.read = true; });
}
export function toggleSidebar() {
appState.sidebar.open = !appState.sidebar.open;
}
Read from the snapshot while rendering; write to the proxy (and read from it inside callbacks).
// components/Header.tsx — Read state with useSnapshot
import { useSnapshot } from "valtio";
import { appState, toggleTheme, toggleSidebar, markAllRead } from "../store/app";
export function Header() {
// useSnapshot creates a read-only snapshot
// Component ONLY re-renders when `user`, `theme` or `unreadCount` changes
// Changes to `sidebar` don't trigger a re-render here
const snap = useSnapshot(appState);
return (
<header>
<button onClick={toggleSidebar}>Menu</button>
<span>{snap.user?.name ?? "Guest"}</span>
<button onClick={markAllRead}>Inbox ({snap.unreadCount})</button>
<button onClick={toggleTheme}>{snap.theme === "light" ? "Dark" : "Light"} mode</button>
</header>
);
}
export function SearchBox({ form }: { form: { query: string } }) {
// `form` is a proxy; sync: true turns off update batching, which controlled inputs need
const snap = useSnapshot(form, { sync: true });
return <input value={snap.query} onChange={(e) => { form.query = e.target.value; }} />;
}
A getter can only read properties of its own object (siblings via this). On the proxy it is recalculated on every access; in a snapshot the value is captured once. In v1 this job was often done with derive from valtio/utils; that export was removed in v2, and both its successor package derive-valtio and the built-in watch util are now deprecated in favour of valtio-reactive (computed, effect, batch). For a value that depends on another proxy, keep it in sync with subscribe:
// store/greeting.ts — derive from a different proxy
import { proxy, subscribe } from "valtio";
import { appState } from "./app";
export const greeting = proxy({ text: "Hello, Guest" });
subscribe(appState, () => {
greeting.text = `Hello, ${appState.user?.name ?? "Guest"}`;
});
// store/effects.ts — Listen to state changes outside components
import { subscribe, snapshot } from "valtio";
import { subscribeKey } from "valtio/utils";
import { appState } from "./app";
// Any change in the tree; mutations in the same tick are batched into one call
const unsubscribe = subscribe(appState, () => {
console.log("State changed:", JSON.stringify(snapshot(appState)));
});
// A nested object can be subscribed to on its own
subscribe(appState.sidebar, () => {
localStorage.setItem("sidebar-open", String(appState.sidebar.open));
});
// A primitive property needs subscribeKey — subscribe() only accepts proxy objects
subscribeKey(appState, "theme", (theme) => {
document.documentElement.dataset.theme = theme;
});
// Mutate from any callback, for example a WebSocket handler
const socket = new WebSocket("wss://api.northwind.dev/notifications");
socket.addEventListener("message", (event) => {
appState.notifications.push(JSON.parse(event.data)); // Components auto-update
});
unsubscribe(); // stop listening when the owner goes away
// components/Release.tsx — a promise in state is unwrapped with React 19's use()
import { Suspense, use } from "react";
import { proxy, ref, useSnapshot } from "valtio";
import { proxyMap, proxySet } from "valtio/utils";
const releaseState = proxy({
latest: fetch("https://registry.npmjs.org/valtio/latest").then(
(res) => res.json() as Promise<{ version: string }>,
),
drafts: proxyMap<string, { title: string }>(), // a native Map or Set is not tracked
selected: proxySet<string>(),
canvas: ref({ context: null as CanvasRenderingContext2D | null }), // stored as-is, never tracked
});
function Version() {
const snap = useSnapshot(releaseState);
return <span>valtio {use(snap.latest).version}</span>;
}
export function Release() {
return <Suspense fallback={<span>Loading</span>}><Version /></Suspense>;
}
On React 18, import use from the react18-use shim instead.
// store/devtools.ts — needs the Redux DevTools browser extension
import type {} from "@redux-devtools/extension"; // types only: npm install -D @redux-devtools/extension
import { devtools } from "valtio/utils";
import { appState } from "./app";
export const disconnect = devtools(appState, { name: "appState", enabled: process.env.NODE_ENV !== "production" });
User prompt: "Build a shopping cart with add/remove/update quantity using simple state management."
// store/cart.tsx
import { proxy, useSnapshot } from "valtio";
interface CartItem { sku: string; name: string; price: number; qty: number }
export const cart = proxy({
items: [] as CartItem[],
get total() {
return this.items.reduce((sum, item) => sum + item.price * item.qty, 0);
},
});
export function addItem(item: Omit<CartItem, "qty">) {
const existing = cart.items.find((i) => i.sku === item.sku);
if (existing) existing.qty += 1;
else cart.items.push({ ...item, qty: 1 });
}
export function setQty(sku: string, qty: number) {
const item = cart.items.find((i) => i.sku === sku);
if (item) item.qty = Math.max(1, qty);
}
export function removeItem(sku: string) {
cart.items = cart.items.filter((i) => i.sku !== sku);
}
export function CartSummary() {
const snap = useSnapshot(cart);
return (
<ul>
{snap.items.map((item) => (
<li key={item.sku}>
{item.name} x {item.qty}
<button onClick={() => setQty(item.sku, item.qty + 1)}>+</button>
<button onClick={() => removeItem(item.sku)}>Remove</button>
</li>
))}
<li>Total: ${snap.total.toFixed(2)}</li>
</ul>
);
}
After addItem({ sku: "TSHIRT-M", name: "T-shirt (M)", price: 24 }) twice and addItem({ sku: "MUG-01", name: "Mug", price: 12 }), the list shows "T-shirt (M) x 2", "Mug x 1" and "Total: $60.00"; the three calls in one tick cause a single re-render.
User prompt: "Store user preferences (theme, language, sidebar state) that persist across page loads."
// store/preferences.ts
import { proxy, subscribe, snapshot } from "valtio";
const STORAGE_KEY = "preferences-v1";
const defaults = { theme: "light" as "light" | "dark", language: "en", sidebarOpen: true };
const saved = typeof localStorage === "undefined" ? null : localStorage.getItem(STORAGE_KEY);
export const preferences = proxy({ ...defaults, ...(saved ? JSON.parse(saved) : {}) } as typeof defaults);
subscribe(preferences, () => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot(preferences)));
});
After preferences.theme = "dark", localStorage.getItem("preferences-v1") returns {"theme":"dark","language":"en","sidebarOpen":true}, and the next page load starts in dark mode. Components read it with useSnapshot(preferences).
proxy() for state, useSnapshot() for reading — read from the snapshot in render, mutate and read the proxy in callbacksstate.count++ works; no setState or set() neededsubscribe() for side effects — persist to localStorage, log, sync; use subscribeKey() for a primitive propertyderive is gone from valtio/utils and watch is deprecated, use valtio-reactive for cross-proxy computeds and effectssnap throws a TypeError, and TypeScript marks it deeply readonly; mutate the original proxypush, splice, sort; filter and map return new arrays, so assign the result back (state.items = state.items.filter(...))state = { ... } drops tracking; and a component subscribed to useSnapshot(state.profile) stops updating if state.profile is reassigned, so snapshot the parent insteadthis in actions — snap.increment() runs against the frozen snapshot and fails; define actions as module functions that reference the proxyproxy() — v2 wraps it in place; pass deepClone(initial) from valtio/utils when you need the original later (for example to reset state)proxyMap/proxySet instead of Map/Set, and wrap DOM nodes, class instances from other libraries and large blobs in ref()useRef(proxy({ ... })).current and pass it through context