Todos Example with Valtio

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')
);