인터페이스는 현지화되어 있으며, Skill 소개와 설명 문서는 선택한 언어로 AI가 자동 번역합니다. 코드·명령어·경로·전문 용어는 원문을 유지하며 원작자 문서가 우선합니다.
Skills 둘러보기
생산성 및 협업 · SKILL.md

valtio

valtio

Valtio 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

Tterminalskills / skillsApache-2.0

Skill 문서

GitHub

name: valtio description: >- Valtio 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 management without boilerplate". Covers Valtio 2: proxy state, snapshots, subscriptions, computed getters, async values with React's use hook, proxyMap/proxySet, and devtools. license: Apache-2.0 compatibility: "Valtio 2.x: React 18 or newer, TypeScript 4.5 or newer. valtio/vanilla runs without React." metadata: author: terminal-skills version: "1.1.0" category: development repository: https://github.com/pmndrs/valtio tags: ["state", "valtio", "react", "proxy", "store"]

Valtio

Overview

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.

When to Use

  • Want the simplest possible state management
  • Tired of Redux boilerplate or Zustand's set() function
  • Sharing state between components without prop drilling
  • State that's accessed/modified outside React (event handlers, WebSocket callbacks)
  • Team prefers mutable patterns over immutable

Instructions

Setup

npm install valtio

Basic Store

// 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;
}

Use in Components

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; }} />;
}

Computed Values

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"}`;
});

Subscribe Outside React

// 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

Async Values, Maps and Non-Proxied Objects

// 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.

Devtools

// 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" });

Examples

Example 1: Build a shopping cart

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.

Example 2: Theme and layout preferences

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).

Guidelines

  • proxy() for state, useSnapshot() for reading — read from the snapshot in render, mutate and read the proxy in callbacks
  • Mutate directly — state.count++ works; no setState or set() needed
  • Automatic render optimization — only re-renders when accessed properties change
  • subscribe() for side effects — persist to localStorage, log, sync; use subscribeKey() for a primitive property
  • Getters for computed values — they may only reference their own object; derive is gone from valtio/utils and watch is deprecated, use valtio-reactive for cross-proxy computeds and effects
  • Snapshot is read-only — writing to snap throws a TypeError, and TypeScript marks it deeply readonly; mutate the original proxy
  • Mutating array methods trigger updates — push, splice, sort; filter and map return new arrays, so assign the result back (state.items = state.items.filter(...))
  • Don't replace the proxy itself — state = { ... } drops tracking; and a component subscribed to useSnapshot(state.profile) stops updating if state.profile is reassigned, so snapshot the parent instead
  • Avoid this in actions — snap.increment() runs against the frozen snapshot and fails; define actions as module functions that reference the proxy
  • Don't reuse the object passed to proxy() — v2 wraps it in place; pass deepClone(initial) from valtio/utils when you need the original later (for example to reset state)
  • Only plain data is tracked — use proxyMap/proxySet instead of Map/Set, and wrap DOM nodes, class instances from other libraries and large blobs in ref()
  • Component-scoped state — a module-level proxy is one shared instance; for state that belongs to a single component tree, create it with useRef(proxy({ ... })).current and pass it through context
  • When not to use it — teams that want explicit, immutable updates are better served by Zustand or Redux Toolkit; server data caching belongs in TanStack Query or SWR
저장한 Skills