Todo List Example

A complete todo list application demonstrating Storken's state management capabilities.

📦 View the complete example on GitHub:

storken/examples/todo-app

Store Configuration

// store.ts
import { create } from 'storken'

interface Todo {
  id: string
  text: string
  completed: boolean
  createdAt: Date
}

type Filter = 'all' | 'active' | 'completed'

interface TodoState {
  todos: Todo[]
  filter: Filter
  searchQuery: string
}

export const [useTodos] = create<TodoState>({
  initialValues: {
    todos: [],
    filter: 'all',
    searchQuery: ''
  },
  
  // Optional: Persist todos to localStorage
  plugins: {
    persist: {
      name: 'persist',
      init(storken) {
        const saved = localStorage.getItem('todos')
        if (saved) {
          storken.set('todos', JSON.parse(saved))
        }
      },
      afterSet(key, value) {
        if (key === 'todos') {
          localStorage.setItem('todos', JSON.stringify(value))
        }
      }
    }
  }
})

Todo List Component

// TodoList.tsx
import { useTodos } from './store'
import { TodoItem } from './TodoItem'
import { AddTodo } from './AddTodo'
import { TodoFilters } from './TodoFilters'

export function TodoList() {
  const [todos, setTodos] = useTodos('todos', [])
  const [filter] = useTodos('filter', 'all')
  const [searchQuery] = useTodos('searchQuery', '')
  
  // Filter todos based on current filter and search
  const filteredTodos = todos
    .filter(todo => {
      if (filter === 'active') return !todo.completed
      if (filter === 'completed') return todo.completed
      return true
    })
    .filter(todo => 
      todo.text.toLowerCase().includes(searchQuery.toLowerCase())
    )
  
  const handleAddTodo = (text: string) => {
    const newTodo: Todo = {
      id: crypto.randomUUID(),
      text,
      completed: false,
      createdAt: new Date()
    }
    setTodos([...todos, newTodo])
  }
  
  const handleToggleTodo = (id: string) => {
    setTodos(todos.map(todo =>
      todo.id === id 
        ? { ...todo, completed: !todo.completed }
        : todo
    ))
  }
  
  const handleDeleteTodo = (id: string) => {
    setTodos(todos.filter(todo => todo.id !== id))
  }
  
  const handleClearCompleted = () => {
    setTodos(todos.filter(todo => !todo.completed))
  }
  
  const activeTodoCount = todos.filter(t => !t.completed).length
  const completedCount = todos.length - activeTodoCount
  
  return (
    <div className="max-w-2xl mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">Todo List</h1>
      
      <AddTodo onAdd={handleAddTodo} />
      <TodoFilters />
      
      <div className="mt-6 space-y-2">
        {filteredTodos.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={handleToggleTodo}
            onDelete={handleDeleteTodo}
          />
        ))}
      </div>
      
      {todos.length > 0 && (
        <div className="mt-6 flex justify-between text-sm text-gray-600">
          <span>{activeTodoCount} active</span>
          {completedCount > 0 && (
            <button
              onClick={handleClearCompleted}
              className="hover:text-red-600"
            >
              Clear completed
            </button>
          )}
        </div>
      )}
    </div>
  )
}

Add Todo Component

// AddTodo.tsx
import { useState } from 'react'

interface AddTodoProps {
  onAdd: (text: string) => void
}

export function AddTodo({ onAdd }: AddTodoProps) {
  const [text, setText] = useState('')
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (text.trim()) {
      onAdd(text.trim())
      setText('')
    }
  }
  
  return (
    <form onSubmit={handleSubmit} className="flex gap-2">
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="What needs to be done?"
        className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
      />
      <button
        type="submit"
        className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
      >
        Add
      </button>
    </form>
  )
}

Filter Component

// TodoFilters.tsx
import { useTodos } from './store'

export function TodoFilters() {
  const [filter, setFilter] = useTodos('filter', 'all')
  const [searchQuery, setSearchQuery] = useTodos('searchQuery', '')
  
  const filters = [
    { value: 'all', label: 'All' },
    { value: 'active', label: 'Active' },
    { value: 'completed', label: 'Completed' }
  ]
  
  return (
    <div className="mt-4 space-y-3">
      <input
        type="text"
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
        placeholder="Search todos..."
        className="w-full px-4 py-2 border rounded-lg"
      />
      
      <div className="flex gap-2">
        {filters.map(f => (
          <button
            key={f.value}
            onClick={() => setFilter(f.value as Filter)}
            className={`px-4 py-2 rounded-lg transition-colors ${
              filter === f.value
                ? 'bg-blue-600 text-white'
                : 'bg-gray-200 hover:bg-gray-300'
            }`}
          >
            {f.label}
          </button>
        ))}
      </div>
    </div>
  )
}

Todo Item Component

// TodoItem.tsx
interface TodoItemProps {
  todo: Todo
  onToggle: (id: string) => void
  onDelete: (id: string) => void
}

export function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
  return (
    <div className="flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg shadow-sm">
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
        className="w-5 h-5 text-blue-600 rounded focus:ring-blue-500"
      />
      
      <span
        className={`flex-1 ${
          todo.completed 
            ? 'line-through text-gray-500' 
            : 'text-gray-900 dark:text-gray-100'
        }`}
      >
        {todo.text}
      </span>
      
      <button
        onClick={() => onDelete(todo.id)}
        className="px-3 py-1 text-red-600 hover:bg-red-50 rounded"
      >
        Delete
      </button>
    </div>
  )
}

Advanced Features

Undo/Redo Functionality

// Add history tracking
const [useTodos, , , sky] = create({
  initialValues: {
    todos: [],
    history: [],
    historyIndex: -1
  }
})

// Undo/Redo functions
export function useTodoHistory() {
  const [history] = useTodos('history', [])
  const [historyIndex, setHistoryIndex] = useTodos('historyIndex', -1)
  const [, setTodos] = useTodos('todos')
  
  const canUndo = historyIndex > 0
  const canRedo = historyIndex < history.length - 1
  
  const undo = () => {
    if (canUndo) {
      const newIndex = historyIndex - 1
      setHistoryIndex(newIndex)
      setTodos(history[newIndex])
    }
  }
  
  const redo = () => {
    if (canRedo) {
      const newIndex = historyIndex + 1
      setHistoryIndex(newIndex)
      setTodos(history[newIndex])
    }
  }
  
  return { undo, redo, canUndo, canRedo }
}

Bulk Operations

// Bulk todo operations
const bulkOperations = {
  toggleAll: () => {
    const [todos, setTodos] = sky.get('todos')
    const allCompleted = todos.every(t => t.completed)
    setTodos(todos.map(t => ({ ...t, completed: !allCompleted })))
  },
  
  deleteCompleted: () => {
    const [todos, setTodos] = sky.get('todos')
    setTodos(todos.filter(t => !t.completed))
  },
  
  sortByDate: () => {
    const [todos, setTodos] = sky.get('todos')
    setTodos([...todos].sort((a, b) => 
      b.createdAt.getTime() - a.createdAt.getTime()
    ))
  }
}

Server Sync

// Sync with backend
const [useTodos] = create({
  initialValues: {
    todos: [],
    syncStatus: 'idle' // 'idle' | 'syncing' | 'error'
  },
  
  getters: {
    todos: async () => {
      const response = await fetch('/api/todos')
      return response.json()
    }
  },
  
  setters: {
    todos: async (storken, todos) => {
      storken.set('syncStatus', 'syncing')
      try {
        await fetch('/api/todos', {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(todos)
        })
        storken.set('syncStatus', 'idle')
      } catch (error) {
        storken.set('syncStatus', 'error')
        throw error
      }
    }
  }
})

✨ Key Features Demonstrated

  • • State sharing across components
  • • Complex state updates with immutability
  • • Filtering and searching
  • • Local storage persistence
  • • TypeScript type safety
  • • Optimistic UI updates