09. Use Case Scenario 3: Valtio
Change detection using proxy and immutable state generation, re-rendering optimization
🔖 1. What is Valtio?
Key Features
A method that uses Proxy to track state and automatically updates only related components when state changes
In other words, you can modify state objects as comfortably as handling them directly, and Valtio detects those changes and re-renders only necessary parts
It wraps state objects with Proxy to detect all access to state. It identifies which parts of state each component reads and re-renders only that component when those parts change
You can read current state snapshots with the
useSnapshot
hook, and automatically receive updated snapshots when state changesSince you can change state as you handle existing JS objects, the learning curve is low
Being Proxy-based, caution may be needed in unpredictable scenarios
Example Code
import { proxy, useSnapshot } from 'valtio';
// 1. State definition: Use proxy function to wrap state with Proxy
const state = proxy({
count: 0,
text: 'Hello',
});
// 2. Component that reads and changes state
function Counter() {
// Get current state snapshot with useSnapshot
const snap = useSnapshot(state);
return (
<div>
<p>Count: {snap.count}</p>
<button onClick={() => state.count++}>+1</button>
</div>
);
}
function TextDisplay() {
const snap = useSnapshot(state);
return (
<div>
<p>Text: {snap.text}</p>
<button onClick={() => (state.text = state.text + '!')}>Add "!"</button>
</div>
);
}
export default function App() {
return (
<div>
<h1>Valtio Example</h1>
<Counter />
<TextDisplay />
</div>
);
}
🔖 2. When is it Good to Use Valtio?
When is it Good to Use Valtio?
When you want to easily define and update global state
When you want to change state intuitively like
state.count++
When is it Better Not to Use Valtio?
When strict patterns are needed in large-scale projects (in such cases, it would be better to use consistent patterns like Redux's action, reducer)
When complex debugging or tracking features are needed like Redux's MobX, Valtio's Proxy-based rendering optimization can become a disadvantage
When type safety is needed, Valtio's loose structure can become a disadvantage
My Thoughts
This chapter introduces Valtio, which takes a different approach to state management by using Proxy for automatic change detection. The key insight is that Valtio provides a more intuitive way to work with state by allowing direct mutations while still maintaining React's optimization benefits.
The discussion about when to use Valtio vs. when not to use it is particularly valuable - it shows that while Valtio is powerful and easy to use, it's not always the best choice for every project.
The Proxy-based approach is interesting because it provides automatic optimization without requiring developers to manually specify dependencies or use selectors.
Code Examples
// ✅ GOOD: Basic Valtio setup
import { proxy, useSnapshot } from 'valtio';
// Simple state object
const state = proxy({
count: 0,
name: 'Ella',
theme: 'light' as 'light' | 'dark',
todos: [] as Todo[],
});
// ✅ GOOD: Components using useSnapshot
function Counter() {
const snap = useSnapshot(state); // Only re-renders when count changes
return (
<div>
<h2>Count: {snap.count}</h2>
<button onClick={() => state.count++}>Increment</button>
<button onClick={() => state.count--}>Decrement</button>
</div>
);
}
function NameDisplay() {
const snap = useSnapshot(state); // Only re-renders when name changes
return (
<div>
<h2>Hello, {snap.name}!</h2>
<input
value={snap.name}
onChange={(e) => (state.name = e.target.value)}
placeholder='Enter name'
/>
</div>
);
}
function ThemeToggle() {
const snap = useSnapshot(state); // Only re-renders when theme changes
return (
<button
onClick={() => (state.theme = snap.theme === 'light' ? 'dark' : 'light')}
>
Current theme: {snap.theme}
</button>
);
}
// ✅ GOOD: Complex state with nested objects
interface User {
id: string;
name: string;
email: string;
preferences: {
theme: 'light' | 'dark';
language: string;
notifications: boolean;
};
}
const userState = proxy<User>({
id: '1',
name: 'Ella',
email: 'ella@example.com',
preferences: {
theme: 'light',
language: 'en',
notifications: true,
},
});
function UserProfile() {
const snap = useSnapshot(userState);
return (
<div>
<h2>{snap.name}</h2>
<p>Email: {snap.email}</p>
<p>Theme: {snap.preferences.theme}</p>
<p>Language: {snap.preferences.language}</p>
</div>
);
}
function UserSettings() {
const snap = useSnapshot(userState);
const updateTheme = (theme: 'light' | 'dark') => {
userState.preferences.theme = theme;
};
const updateLanguage = (language: string) => {
userState.preferences.language = language;
};
const toggleNotifications = () => {
userState.preferences.notifications = !snap.preferences.notifications;
};
return (
<div>
<h3>Settings</h3>
<div>
<label>
Theme:
<select
value={snap.preferences.theme}
onChange={(e) => updateTheme(e.target.value as 'light' | 'dark')}
>
<option value='light'>Light</option>
<option value='dark'>Dark</option>
</select>
</label>
</div>
<div>
<label>
Language:
<input
value={snap.preferences.language}
onChange={(e) => updateLanguage(e.target.value)}
/>
</label>
</div>
<div>
<label>
<input
type='checkbox'
checked={snap.preferences.notifications}
onChange={toggleNotifications}
/>
Enable notifications
</label>
</div>
</div>
);
}
// ✅ GOOD: Array state management
interface Todo {
id: string;
text: string;
completed: boolean;
}
const todoState = proxy({
todos: [] as Todo[],
filter: 'all' as 'all' | 'active' | 'completed',
});
function TodoList() {
const snap = useSnapshot(todoState);
const addTodo = (text: string) => {
const newTodo: Todo = {
id: Date.now().toString(),
text,
completed: false,
};
todoState.todos.push(newTodo);
};
const toggleTodo = (id: string) => {
const todo = todoState.todos.find((t) => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
};
const removeTodo = (id: string) => {
const index = todoState.todos.findIndex((t) => t.id === id);
if (index !== -1) {
todoState.todos.splice(index, 1);
}
};
const filteredTodos = snap.todos.filter((todo) => {
if (snap.filter === 'active') return !todo.completed;
if (snap.filter === 'completed') return todo.completed;
return true;
});
return (
<div>
<div>
<input
placeholder='Add new todo'
onKeyPress={(e) => {
if (e.key === 'Enter' && e.currentTarget.value.trim()) {
addTodo(e.currentTarget.value.trim());
e.currentTarget.value = '';
}
}}
/>
</div>
<div>
<button onClick={() => (todoState.filter = 'all')}>All</button>
<button onClick={() => (todoState.filter = 'active')}>Active</button>
<button onClick={() => (todoState.filter = 'completed')}>
Completed
</button>
</div>
<ul>
{filteredTodos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => toggleTodo(todo.id)}
onRemove={() => removeTodo(todo.id)}
/>
))}
</ul>
</div>
);
}
function TodoItem({
todo,
onToggle,
onRemove,
}: {
todo: Todo;
onToggle: () => void;
onRemove: () => void;
}) {
return (
<li>
<input type='checkbox' checked={todo.completed} onChange={onToggle} />
<span
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</span>
<button onClick={onRemove}>Delete</button>
</li>
);
}
// ✅ GOOD: Async state management
const asyncState = proxy({
data: null as any,
loading: false,
error: null as string | null,
});
async function fetchData() {
asyncState.loading = true;
asyncState.error = null;
try {
const response = await fetch('/api/data');
const data = await response.json();
asyncState.data = data;
} catch (error) {
asyncState.error = error.message;
} finally {
asyncState.loading = false;
}
}
function DataDisplay() {
const snap = useSnapshot(asyncState);
useEffect(() => {
fetchData();
}, []);
if (snap.loading) return <div>Loading...</div>;
if (snap.error) return <div>Error: {snap.error}</div>;
if (!snap.data) return <div>No data</div>;
return (
<div>
<h2>Data Loaded</h2>
<pre>{JSON.stringify(snap.data, null, 2)}</pre>
</div>
);
}
// ✅ GOOD: Custom hooks for Valtio
function useValtioState<T>(proxyState: T) {
const snap = useSnapshot(proxyState);
return [snap, proxyState] as const;
}
function useValtioSelector<T, R>(proxyState: T, selector: (state: T) => R): R {
const snap = useSnapshot(proxyState);
return selector(snap);
}
// Usage example
function OptimizedCounter() {
const count = useValtioSelector(state, (s) => s.count); // Only re-renders when count changes
return (
<div>
<p>Count: {count}</p>
<button onClick={() => state.count++}>Increment</button>
</div>
);
}
Last updated