valtio

$npx mdskill add TerminalSkills/skills/valtio

Simplify React state management with Valtio's proxy-based approach

  • Solves the need for simple, boilerplate-free state management in React apps
  • Uses Valtio's proxy-based API and works with React 18+, Vue, and Svelte
  • Tracks component dependencies and re-renders only when relevant state changes
  • Provides mutable state access with automatic subscription and computed values
SKILL.md
.github/skills/valtioView on GitHub ↗
---
name: valtio
description: >-
  Manage React state with Valtio — proxy-based state that just works. Use when
  someone asks to "simple React state management", "Valtio", "proxy state",
  "mutable state in React", "alternative to Zustand/Redux", or "state
  management without boilerplate". Covers proxy state, subscriptions, computed
  values, and devtools.
license: Apache-2.0
compatibility: "React 18+. Also works with vanilla JS, Vue, Svelte."
metadata:
  author: terminal-skills
  version: "1.0.0"
  category: development
  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.

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

```bash
npm install valtio
```

### Basic Store

```typescript
// store/app.ts — Define state as a plain object
import { proxy, useSnapshot } 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 },
});

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

```tsx
// components/Header.tsx — Read state with useSnapshot
import { useSnapshot } from "valtio";
import { appState, toggleTheme, toggleSidebar } from "@/store/app";

export function Header() {
  // useSnapshot creates a read-only snapshot
  // Component ONLY re-renders when `user` or `theme` changes
  // Changes to `notifications` or `sidebar` don't trigger re-render here
  const snap = useSnapshot(appState);

  return (
    <header className="flex items-center justify-between p-4">
      <button onClick={toggleSidebar}>☰</button>
      <span>{snap.user?.name ?? "Guest"}</span>
      <button onClick={toggleTheme}>
        {snap.theme === "light" ? "🌙" : "☀️"}
      </button>
    </header>
  );
}
```

```tsx
// components/NotificationBell.tsx — Derived/computed values
import { useSnapshot } from "valtio";
import { appState, markAllRead } from "@/store/app";

export function NotificationBell() {
  const snap = useSnapshot(appState);
  const unread = snap.notifications.filter((n) => !n.read).length;

  return (
    <button onClick={markAllRead} className="relative">
      🔔
      {unread > 0 && (
        <span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
          {unread}
        </span>
      )}
    </button>
  );
}
```

### Computed Values with derive

```typescript
// store/derived.ts — Computed values that auto-update
import { derive } from "valtio/utils";
import { appState } from "./app";

export const derived = derive({
  unreadCount: (get) => get(appState).notifications.filter((n) => !n.read).length,
  isDarkMode: (get) => get(appState).theme === "dark",
  isLoggedIn: (get) => get(appState).user !== null,
});
```

### Subscribe Outside React

```typescript
// Listen to state changes outside components
import { subscribe } from "valtio";
import { appState } from "./store/app";

// Log every state change
subscribe(appState, () => {
  console.log("State changed:", JSON.stringify(appState));
});

// Subscribe to specific property
subscribe(appState.sidebar, () => {
  localStorage.setItem("sidebar-open", String(appState.sidebar.open));
});

// Use in WebSocket handler
socket.on("notification", (data) => {
  appState.notifications.push(data);  // Components auto-update
});
```

## Examples

### Example 1: Build a shopping cart

**User prompt:** "Build a shopping cart with add/remove/update quantity using simple state management."

The agent will create a Valtio proxy for cart state, actions that directly mutate the array, and components that reactively display cart contents and total.

### Example 2: Theme and layout preferences

**User prompt:** "Store user preferences (theme, language, sidebar state) that persist across page loads."

The agent will create a Valtio store with localStorage sync via subscribe, and components that read preferences reactively.

## Guidelines

- **`proxy()` for state, `useSnapshot()` for reading** — always use snapshot in components
- **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
- **`derive()` for computed values** — auto-recalculates when dependencies change
- **Works outside React** — mutate from event handlers, WebSocket, timers
- **Snapshot is read-only** — don't mutate `snap`, mutate the original `proxy`
- **Arrays work naturally** — `push`, `splice`, `filter` all trigger re-renders
- **Nested objects tracked** — `state.user.name = "new"` triggers re-render
- **Devtools** — `import { devtools } from "valtio/utils"` for Redux DevTools integration
More from TerminalSkills/skills