Todo List Example
A complete todo list application demonstrating Storken's state management capabilities.
📦 View the complete example on GitHub:
storken/examples/todo-appStore 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