The classic Todo app built the right way — with clean state management, persistence, and filtering.
// context/todoReducer.js
export function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, { id: crypto.randomUUID(), text: action.text, done: false }];
case 'TOGGLE':
return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
case 'DELETE':
return state.filter(t => t.id !== action.id);
case 'CLEAR_COMPLETED':
return state.filter(t => !t.done);
default:
return state;
}
}
// context/TodoContext.jsx
const TodoContext = createContext(null);
export function TodoProvider({ children }) {
const [todos, dispatch] = useReducer(
todoReducer,
[],
() => JSON.parse(localStorage.getItem('todos') ?? '[]')
);
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
return (
<TodoContext.Provider value={{ todos, dispatch }}>
{children}
</TodoContext.Provider>
);
}
export const useTodos = () => useContext(TodoContext);
function TodoList() {
const { todos, dispatch } = useTodos();
const [filter, setFilter] = useState('all');
const visible = todos.filter(t =>
filter === 'active' ? !t.done :
filter === 'completed' ? t.done : true
);
return (
<div>
{visible.map(todo => (
<div key={todo.id} className={todo.done ? 'done' : ''}>
<span onClick={() => dispatch({ type: 'TOGGLE', id: todo.id })}>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'DELETE', id: todo.id })}>✕</button>
</div>
))}
<div>
{['all', 'active', 'completed'].map(f => (
<button key={f} onClick={() => setFilter(f)}
style={{ fontWeight: filter === f ? 'bold' : 'normal' }}>
{f}
</button>
))}
</div>
</div>
);
}
All Comments