スキル一覧に戻る
mgd34msu

valtio

by mgd34msu

Plug in, Receive good vibes.

2🍴 0📅 2026年1月25日
GitHubで見るManusで実行

SKILL.md


name: valtio description: Manages state with Valtio using proxy-based reactivity, direct mutations, and automatic re-renders. Use when wanting mutable state syntax, fine-grained reactivity, or state management outside React components.

Valtio

Proxy-based state management that makes React state feel like plain JavaScript.

Quick Start

Install:

npm install valtio

Create state:

// store.ts
import { proxy } from 'valtio';

interface Store {
  count: number;
  users: User[];
  filter: 'all' | 'active' | 'completed';
}

export const store = proxy<Store>({
  count: 0,
  users: [],
  filter: 'all',
});

// Actions - mutate directly
export const increment = () => {
  store.count++;
};

export const addUser = (user: User) => {
  store.users.push(user);
};

Use in React:

import { useSnapshot } from 'valtio';
import { store, increment } from './store';

function Counter() {
  // Only re-renders when count changes
  const snap = useSnapshot(store);

  return (
    <div>
      <p>Count: {snap.count}</p>
      <button onClick={increment}>+1</button>
      {/* Or mutate directly */}
      <button onClick={() => store.count++}>+1</button>
    </div>
  );
}

Core Concepts

proxy() - Create Reactive State

import { proxy } from 'valtio';

// Simple state
const state = proxy({ count: 0, text: '' });

// Nested objects are automatically proxied
const store = proxy({
  user: {
    name: 'John',
    settings: {
      theme: 'dark',
      notifications: true,
    },
  },
  todos: [],
});

// Mutations work at any depth
store.user.settings.theme = 'light';
store.todos.push({ id: 1, text: 'Learn Valtio' });

useSnapshot() - Read in React

Returns a frozen, read-only snapshot that triggers re-renders only when accessed properties change.

import { useSnapshot } from 'valtio';

function UserProfile() {
  const snap = useSnapshot(store);

  // Only re-renders when user.name changes
  return <p>{snap.user.name}</p>;
}

function TodoList() {
  const snap = useSnapshot(store);

  // Only re-renders when todos array changes
  return (
    <ul>
      {snap.todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

Synchronous updates:

// For immediate renders (useful in tests)
const snap = useSnapshot(store, { sync: true });

Mutations - Write to Proxy

Always mutate the proxy, never the snapshot.

// Direct mutations
store.count++;
store.user.name = 'Jane';

// Array mutations
store.items.push(newItem);
store.items.splice(index, 1);
store.items[0].done = true;

// Object replacement
store.user = { ...store.user, name: 'Jane' };

// Delete properties
delete store.user.email;

Actions Pattern

// store/todos.ts
import { proxy } from 'valtio';

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

interface TodoStore {
  todos: Todo[];
  filter: 'all' | 'active' | 'completed';
}

export const todoStore = proxy<TodoStore>({
  todos: [],
  filter: 'all',
});

// Actions - define alongside store
export const actions = {
  addTodo(text: string) {
    todoStore.todos.push({
      id: crypto.randomUUID(),
      text,
      completed: false,
    });
  },

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

  removeTodo(id: string) {
    const index = todoStore.todos.findIndex(t => t.id === id);
    if (index >= 0) {
      todoStore.todos.splice(index, 1);
    }
  },

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

  setFilter(filter: TodoStore['filter']) {
    todoStore.filter = filter;
  },
};

Computed Properties

Getters

const store = proxy({
  firstName: 'John',
  lastName: 'Doe',

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },

  todos: [] as Todo[],

  get completedCount() {
    return this.todos.filter(t => t.completed).length;
  },

  get activeCount() {
    return this.todos.length - this.completedCount;
  },
});

// Usage
console.log(store.fullName); // 'John Doe'

derive() - Cross-Store Computations

import { proxy } from 'valtio';
import { derive } from 'derive-valtio';

const userStore = proxy({ name: 'John', role: 'admin' });
const settingsStore = proxy({ theme: 'dark' });

// Create derived state from multiple stores
const derived = derive({
  greeting: (get) => `Hello, ${get(userStore).name}!`,
  canEdit: (get) => get(userStore).role === 'admin',
});

// Attach derived properties to existing proxy
derive(
  {
    isDark: (get) => get(settingsStore).theme === 'dark',
  },
  { proxy: userStore }
);

Subscriptions

subscribe() - React to Changes

import { subscribe } from 'valtio';

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

// Subscribe to nested object
subscribe(store.user, () => {
  console.log('User changed');
});

// Persist to localStorage
subscribe(store, () => {
  localStorage.setItem('store', JSON.stringify(store));
});

// Cleanup
unsubscribe();

subscribeKey() - Watch Single Property

import { subscribeKey } from 'valtio/utils';

subscribeKey(store, 'count', (value) => {
  console.log('Count is now:', value);
  document.title = `Count: ${value}`;
});

watch() - Auto-tracking

import { watch } from 'valtio/utils';

const stop = watch((get) => {
  // Automatically subscribes to accessed properties
  console.log('User:', get(store).user.name);
  console.log('Count:', get(store).count);
});

// Later
stop();

Utilities

ref() - Escape Proxy

Wrap values that shouldn't be proxied (DOM nodes, class instances, large data).

import { proxy, ref } from 'valtio';

const store = proxy({
  // DOM node - don't proxy
  canvas: ref(document.createElement('canvas')),

  // Large dataset - don't proxy for performance
  bigData: ref(hugeArray),

  // Class instance - preserve prototype
  date: ref(new Date()),
});

snapshot() - Get Immutable Copy

import { snapshot } from 'valtio';

const snap = snapshot(store);

// Useful for:
// - Sending to API
// - Logging
// - Comparison
console.log(JSON.stringify(snap));

// Deep equality check
if (snapshot(store) !== previousSnap) {
  // State changed
}

proxySet() and proxyMap()

import { proxySet, proxyMap } from 'valtio/utils';

// Reactive Set
const selectedIds = proxySet<string>(['id1', 'id2']);
selectedIds.add('id3');
selectedIds.delete('id1');
selectedIds.has('id2'); // true

// Reactive Map
const users = proxyMap<string, User>([
  ['user1', { name: 'John' }],
]);
users.set('user2', { name: 'Jane' });
users.get('user1'); // { name: 'John' }
users.delete('user1');

devtools()

import { devtools } from 'valtio/utils';

// Connect to Redux DevTools
const unsub = devtools(store, {
  name: 'MyApp Store',
  enabled: process.env.NODE_ENV === 'development',
});

Async Actions

const store = proxy({
  users: [] as User[],
  loading: false,
  error: null as string | null,
});

export async function fetchUsers() {
  store.loading = true;
  store.error = null;

  try {
    const response = await fetch('/api/users');
    store.users = await response.json();
  } catch (e) {
    store.error = e instanceof Error ? e.message : 'Unknown error';
  } finally {
    store.loading = false;
  }
}

// Can call from anywhere - not just React
fetchUsers();

Outside React

Valtio works without React.

import { proxy, subscribe, snapshot } from 'valtio/vanilla';

const state = proxy({ count: 0 });

// Subscribe to changes
subscribe(state, () => {
  const snap = snapshot(state);
  document.getElementById('count')!.textContent = String(snap.count);
});

// Update from anywhere
document.getElementById('btn')!.onclick = () => {
  state.count++;
};

Testing

import { proxy, snapshot } from 'valtio';
import { store, actions } from './store';

describe('todoStore', () => {
  beforeEach(() => {
    // Reset state
    store.todos = [];
    store.filter = 'all';
  });

  it('adds todo', () => {
    actions.addTodo('Test todo');

    expect(store.todos).toHaveLength(1);
    expect(store.todos[0].text).toBe('Test todo');
  });

  it('toggles todo', () => {
    actions.addTodo('Test');
    const id = store.todos[0].id;

    actions.toggleTodo(id);

    expect(store.todos[0].completed).toBe(true);
  });

  it('creates immutable snapshot', () => {
    store.count = 5;
    const snap = snapshot(store);

    expect(snap.count).toBe(5);
    expect(() => {
      (snap as any).count = 10;
    }).toThrow();
  });
});

Common Patterns

Store Slices

// stores/user.ts
export const userStore = proxy({
  user: null as User | null,
  login(user: User) {
    this.user = user;
  },
  logout() {
    this.user = null;
  },
});

// stores/cart.ts
export const cartStore = proxy({
  items: [] as CartItem[],
  add(item: CartItem) {
    this.items.push(item);
  },
});

// stores/index.ts - combine if needed
export { userStore } from './user';
export { cartStore } from './cart';

Form State

const formStore = proxy({
  values: {
    email: '',
    password: '',
  },
  errors: {} as Record<string, string>,
  touched: {} as Record<string, boolean>,

  setField(field: string, value: string) {
    this.values[field] = value;
    this.touched[field] = true;
    this.validate(field);
  },

  validate(field?: string) {
    // Validation logic
  },
});

Best Practices

  1. Read from snapshot, write to proxy - Never mutate snap
  2. Define actions as functions - Keep mutations organized
  3. Use ref() for non-reactive data - DOM nodes, large arrays
  4. Keep stores small - Split by domain/feature
  5. TypeScript interfaces - Define types for store shape

Common Mistakes

MistakeFix
Mutating snapshotMutate proxy instead
Spreading proxy in renderUse snapshot values
Large objects without ref()Wrap with ref()
Async in renderMove to action function

Reference Files

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です