Getting Started with Valtio
Let's learn Valtio by building a simple to-do app in React and TypeScript. This guide will walk you through the core concepts and show you how to manage both simple and more complex state.
The To-Do App Example
1. Define the State with proxy
First, we'll define the shape of our state and create a reactive proxy store. This store will hold our list of todos and the current filter.
import { proxy } from 'valtio';
type Status = 'pending' | 'completed';
type Filter = Status | 'all';
type Todo = {
description: string;
status: Status;
id: number;
};
export const store = proxy<{ filter: Filter; todos: Todo[] }>({
filter: 'all',
todos: [],
});
2. Read the State with useSnapshot
To display our todos in a React component, we use the useSnapshot
hook. This creates an immutable snapshot of the state. The component will only re-render if the properties it accessed (todos
and filter
) have changed.
import { useSnapshot } from 'valtio';
const Todos = () => {
const snap = useSnapshot(store);
return (
<ul>
{snap.todos
.filter(({ status }) => status === snap.filter || snap.filter === 'all')
.map(({ description, status, id }) => {
return (
<li key={id}>
<span data-status={status} className="description">
{description}
</span>
<button className="remove" onClick={() => removeTodo(id)}>x</button>
</li>
);
})}
</ul>
);
};
3. Mutate the State with Actions
To add, update, or delete todos, we directly mutate the store
object. It's a common pattern to wrap these mutations in action functions.
Important: Always mutate the original store
proxy, not the snap
object returned by useSnapshot
.
const addTodo = (description: string) => {
store.todos.push({
description,
status: 'pending',
id: Date.now(),
});
};
const removeTodo = (id: number) => {
const index = store.todos.findIndex((todo) => todo.id === id);
if (index >= 0) {
store.todos.splice(index, 1);
}
};
const toggleDone = (id: number, currentStatus: Status) => {
const nextStatus = currentStatus === 'pending' ? 'completed' : 'pending';
const todo = store.todos.find((todo) => todo.id === id);
if (todo) {
todo.status = nextStatus;
}
};
const setFilter = (filter: Filter) => {
store.filter = filter;
};
Now you can wire up these actions to your UI components.
Complete Example: Simple To-Do App
Explore the full code for this basic To-Do app in the CodeSandbox below.
https://codesandbox.io/s/valtio-to-do-list-forked-6w9h3z
Mutating State Outside of Components
One of Valtio's powerful features is the ability to mutate state from anywhere, even outside of React components. Let's enhance our to-do app with a countdown timer for each task.
First, we'll add a timeLeft
property to our Todo
type.
type Todo = {
description: string;
status: Status;
id: number;
timeLeft: number; // Time in milliseconds
};
Next, we'll create a Countdown
component that displays the remaining time for a todo. It takes the todo's index as a prop and creates a snapshot of that specific todo object for optimized re-renders.
import { useSnapshot } from 'valtio';
import { store } from './App';
export const Countdown = ({ index }: { index: number }) => {
const snap = useSnapshot(store.todos[index]);
// ... logic to format and display snap.timeLeft ...
};
Now for the magic. We can create a recursive countdown
function in module scope (outside any component) that directly mutates the timeLeft
property for a given todo.
const countdown = (index: number) => {
const todo = store.todos[index];
// Exit if todo is removed, completed, or overdue
if (!todo || todo.status !== 'pending') return;
// Handle timer expiration
if (todo.timeLeft < 1000) {
todo.timeLeft = 0;
todo.status = 'overdue';
return;
}
// Decrement time and schedule the next tick
setTimeout(() => {
todo.timeLeft -= 1000;
countdown(index);
}, 1000);
};
We can start this countdown from our addTodo
action.
const addTodo = (description: string, deadline: Date) => {
const now = Date.now();
store.todos.push({
description,
status: 'pending',
id: now,
timeLeft: deadline.getTime() - now,
});
// Start the countdown for the newly added todo
countdown(store.todos.length - 1);
};
This approach cleanly separates the timer logic from the UI, demonstrating how Valtio simplifies complex, asynchronous state management.
Complete Example: Countdown To-Do App
See the full implementation of the countdown feature in this CodeSandbox.
https://codesandbox.io/s/valtio-countdown-to-do-list-xkgmri