Todo List
A classic todo list that demonstrates atomFamily for individual items, family subscriptions for the list, and selector for derived counts.
Shared state
This state definition is the same regardless of which framework you use:
import { atom, atomFamily, selector } from "valdres"
export const todoAtom = atomFamily()
export const todoIdsAtom = atom([])
export const remainingCountSelector = selector(get => {
const ids = get(todoIdsAtom)
return ids.filter(id => !get(todoAtom(id)).done).length
})
export function addTodo(store, title) {
const id = crypto.randomUUID()
store.txn(set => {
set(todoAtom(id), { id, title, done: false })
set(todoIdsAtom, ids => [...ids, id])
})
return id
}
export function toggleTodo(store, id) {
store.set(todoAtom(id), prev => ({ ...prev, done: !prev.done }))
}
export function removeTodo(store, id) {
store.txn(set => {
set(todoIdsAtom, ids => ids.filter(i => i !== id))
})
}
Why atomFamily?
Each todo lives in its own atom. When you toggle one todo, only the component rendering that specific todo re-renders — not the entire list. This gives you fine-grained updates for free.React
import { useValue, useStore } from "valdres-react"
import { todoAtom, todoIdsAtom, remainingCountSelector, addTodo, toggleTodo, removeTodo } from "./state"
function TodoApp() {
const store = useStore()
const ids = useValue(todoIdsAtom)
const remaining = useValue(remainingCountSelector)
const handleAdd = (e) => {
e.preventDefault()
const input = e.target.elements.title
if (input.value.trim()) {
addTodo(store, input.value.trim())
input.value = ""
}
}
return (
<div>
<h2>{remaining} items remaining</h2>
<form onSubmit={handleAdd}>
<input name="title" placeholder="What needs to be done?" />
<button type="submit">Add</button>
</form>
<ul>
{ids.map(id => <TodoItem key={id} id={id} />)}
</ul>
</div>
)
}
function TodoItem({ id }) {
const store = useStore()
const todo = useValue(todoAtom(id))
return (
<li>
<label>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(store, id)}
/>
<span style={{ textDecoration: todo.done ? "line-through" : "none" }}>
{todo.title}
</span>
</label>
<button onClick={() => removeTodo(store, id)}>×</button>
</li>
)
}
Vue
<script setup>
import { useValue, useStore } from "valdres-vue"
import { todoAtom, todoIdsAtom, remainingCountSelector, addTodo, toggleTodo, removeTodo } from "./state"
const store = useStore()
const ids = useValue(todoIdsAtom)
const remaining = useValue(remainingCountSelector)
const newTitle = ref("")
function handleAdd() {
if (newTitle.value.trim()) {
addTodo(store, newTitle.value.trim())
newTitle.value = ""
}
}
</script>
<template>
<div>
<h2>{{ remaining }} items remaining</h2>
<form @submit.prevent="handleAdd">
<input v-model="newTitle" placeholder="What needs to be done?" />
<button type="submit">Add</button>
</form>
<ul>
<TodoItem v-for="id in ids" :key="id" :id="id" />
</ul>
</div>
</template>
<!-- TodoItem.vue -->
<script setup>
import { useValue, useStore } from "valdres-vue"
import { todoAtom, toggleTodo, removeTodo } from "./state"
const props = defineProps(['id'])
const store = useStore()
const todo = useValue(todoAtom(props.id))
</script>
<template>
<li>
<label>
<input type="checkbox" :checked="todo.done" @change="toggleTodo(store, id)" />
<span :style="{ textDecoration: todo.done ? 'line-through' : 'none' }">
{{ todo.title }}
</span>
</label>
<button @click="removeTodo(store, id)">×</button>
</li>
</template>
Svelte
<script>
import { watch, readable } from "valdres-svelte"
import { todoAtom, todoIdsAtom, remainingCountSelector, addTodo, toggleTodo, removeTodo } from "./state"
import { getValdresContext } from "valdres-svelte"
const store = getValdresContext()
const ids = readable(todoIdsAtom)
const remaining = readable(remainingCountSelector)
let newTitle = $state("")
function handleAdd(e) {
e.preventDefault()
if (newTitle.trim()) {
addTodo(store, newTitle.trim())
newTitle = ""
}
}
</script>
<div>
<h2>{remaining.value} items remaining</h2>
<form onsubmit={handleAdd}>
<input bind:value={newTitle} placeholder="What needs to be done?" />
<button type="submit">Add</button>
</form>
<ul>
{#each ids.value as id (id)}
{@const todo = watch(todoAtom(id))}
<li>
<label>
<input type="checkbox" checked={todo.value.done} onchange={() => toggleTodo(store, id)} />
<span style:text-decoration={todo.value.done ? "line-through" : "none"}>
{todo.value.title}
</span>
</label>
<button onclick={() => removeTodo(store, id)}>×</button>
</li>
{/each}
</ul>
</div>
Solid
import { createValue, useStore } from "valdres-solid"
import { todoAtom, todoIdsAtom, remainingCountSelector, addTodo, toggleTodo, removeTodo } from "./state"
import { For } from "solid-js"
function TodoApp() {
const store = useStore()
const ids = createValue(todoIdsAtom)
const remaining = createValue(remainingCountSelector)
let inputRef
const handleAdd = (e) => {
e.preventDefault()
if (inputRef.value.trim()) {
addTodo(store, inputRef.value.trim())
inputRef.value = ""
}
}
return (
<div>
<h2>{remaining()} items remaining</h2>
<form onSubmit={handleAdd}>
<input ref={inputRef} placeholder="What needs to be done?" />
<button type="submit">Add</button>
</form>
<ul>
<For each={ids()}>
{id => <TodoItem id={id} />}
</For>
</ul>
</div>
)
}
function TodoItem(props) {
const store = useStore()
const todo = createValue(todoAtom(props.id))
return (
<li>
<label>
<input
type="checkbox"
checked={todo().done}
onChange={() => toggleTodo(store, props.id)}
/>
<span style={{ "text-decoration": todo().done ? "line-through" : "none" }}>
{todo().title}
</span>
</label>
<button onClick={() => removeTodo(store, props.id)}>×</button>
</li>
)
}
Angular
import { Component, signal, computed } from "@angular/core"
import { injectValue, injectStore } from "valdres-angular"
import { todoAtom, todoIdsAtom, remainingCountSelector, addTodo, toggleTodo, removeTodo } from "./state"
@Component({
selector: "todo-app",
template: `
<div>
<h2>{{ remaining() }} items remaining</h2>
<form (submit)="handleAdd($event)">
<input #titleInput placeholder="What needs to be done?" />
<button type="submit">Add</button>
</form>
<ul>
@for (id of ids(); track id) {
<todo-item [todoId]="id" />
}
</ul>
</div>
`,
})
export class TodoApp {
store = injectStore()
ids = injectValue(todoIdsAtom)
remaining = injectValue(remainingCountSelector)
handleAdd(e: Event) {
e.preventDefault()
const input = (e.target as HTMLFormElement).elements.namedItem("title") as HTMLInputElement
if (input.value.trim()) {
addTodo(this.store, input.value.trim())
input.value = ""
}
}
}
@Component({
selector: "todo-item",
template: `
<li>
<label>
<input type="checkbox" [checked]="todo().done" (change)="toggle()" />
<span [style.text-decoration]="todo().done ? 'line-through' : 'none'">
{{ todo().title }}
</span>
</label>
<button (click)="remove()">×</button>
</li>
`,
inputs: ["todoId"],
})
export class TodoItem {
store = injectStore()
todoId!: string
todo = computed(() => injectValue(todoAtom(this.todoId))())
toggle() { toggleTodo(this.store, this.todoId) }
remove() { removeTodo(this.store, this.todoId) }
}
Key takeaways
- Fine-grained updates — each todo is its own atom. Toggling one doesn't re-render the others.
- Transactions — adding a todo updates both the item atom and the ID list atomically.
- Derived state — the remaining count auto-updates when any todo changes.
- Framework agnostic state — the entire state layer (
state.ts) is identical across all frameworks. Only the UI binding changes.