Todos Example with Valtio
Learn how to use Valtio through a basic Todos example.
Creating a state
Creating a state is just wrapping an object with proxy
. Any updates to the state
object is trapped and handled by the library.
import { proxy } from 'valtio';
const state = proxy({
filter: 'all',
todos: [],
});
In this case, the nested todos
array is also wrapped with another proxy without explicit proxy
in the code.
Defining actions
Although not necessary, we define some actions–which are functions,–to mutate state
.
let todoId = 0;
const addTodo = (title, completed) => {
if (!title) {
return;
}
const id = ++todoId;
state.todos.push({ id, title, completed })
};
const removeTodo = (id) => {
state.todos = state.todos.filter((todo) => todo.id !== id);
};
const toggleTodo = (id) => {
const todo = state.todos.find((todo) => todo.id === id);
todo.completed = !todo.completed;
};
Even if we don’t define actions, we can directly mutate state
from anywhere.
Note: Do not mutate
state
in React render function. This rule is not limited to Valtio.
Defining custom hooks
This is optional too, but we define a custom hook to extract some logic. In React custom hooks as well as render functions, we use useProxy
.
import { useSnapshot } from 'valtio';
const useFilteredTodos = () => {
const { filter, todos } = useSnapshot(state);
if (filter === 'all') {
return todos;
}
if (filter === 'completed') {
return todos.filter((todo) => todo.completed);
}
return todos.filter((todo) => !todo.completed)
};
The value returned by useSnapshot
is an immutable object created from state
.
Using the snapshot in render allows render optimization. It will trigger re-renders only if used parts are changed.
For example, suppose a state is defined like this:
const state = proxy({ a: 1, b: 2 });
In the following component, only the property a
of the state
is used.
const Component = () => {
const snap = useSnapshot(state);
return <div>a: {snap.a}</div>;
};
This component will not re-render when only state.b
is changed.
Defining TodoItem
component
const TodoItem = ({ todo }) => (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : '' }}>
{todo.title}
</span>
<button onClick={() => removeTodo(todo.id)}>x</button>
</div>
);
This is a normal React component, albiet with one caveat: using with Valtio’s useSnapshot, we get a special todo
prop for render optimization. In this case, we don’t need to wrap the component with React.memo
. In fact, we should not wrap it to make the render optimization properly.
In the case when one needs more fine-grained render optimization with React.memo
, the component should receive primitive values in props instead of object values.
const TodoItem = memo(({ id, title, completed }) => (
// ...
))
Defining the Filter
component
const Filter = () => {
const { filter } = useSnapshot(state);
const handleChange = (e) => {
state.filter = e.target.value;
};
return (
<div>
<label>
<input type="radio" value="all" checked={filter === 'all'} onChange={handleChange} />
All
</label>
<label>
<input type="radio" value="completed" checked={filter === 'completed'} onChange={handleChange} />
Completed
</label>
<label>
<input type="radio" value="incompleted" checked={filter === 'incompleted'} onChange={handleChange} />
Incompleted
</label>
</div>
)
};
This component uses useSnapshot
and it’s totally valid. It’s a useful pattern in small components. Notice that we use state
to mutate in a callback.
Defining the TodoList
component
const TodoList = () => {
const filtered = useFilteredTodos();
const add = (e) => {
e.preventDefault();
const title = e.target.title.value;
e.target.title.value = '';
addTodo(title, false);
};
return (
<div>
<Filter />
<form onSubmit={add}>
<input name="title" placeholder="Enter title..." />
</form>
{filtered.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
);
};
We use the custom hook we defined earlier. Other than that, it’s just like a normal React component.
We are all set to run this example.
import React from 'react'; require('./style.css'); import ReactDOM from 'react-dom'; import App from './app.js'; ReactDOM.render( <App />, document.getElementById('root') );