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.