Most todo apps are throwaway projects. I built Toodle to be something I'd actually use — and to push myself on three things I wanted to get better at: TypeScript strictness, real-time data with Firebase, and data visualization.
What the App Does
Toodle lets you create, organize, and track tasks with real-time sync across devices. The interesting part isn't the task management itself — it's the analytics layer on top. You get a dashboard showing completion rates, productivity trends, and task distribution across categories.
The live version is at toodle-hbich.vercel.app and the code is on GitHub.
Stack Choices
React + TypeScript was a deliberate choice. I wanted every piece of state and every prop to be fully typed. No any, no shortcuts. It slows you down at first but the feedback loop in VS Code becomes genuinely useful — you catch bugs before they exist.
Firebase for the backend because the use case is a perfect fit. Real-time listeners mean any change (task added, completed, deleted) updates every open tab instantly with zero polling. Firestore's document model also maps cleanly to a task list — each task is a document, each list is a collection.
Mantine UI for components and Mantine Charts for the analytics dashboard. I chose Mantine over shadcn/ui here because the chart library is first-party and shares the same theming system. Less configuration to connect two separate libraries.
React Router v7 for navigation between the task view and the analytics dashboard.
The Hardest Part: Real-Time State Management
The challenge with Firebase real-time listeners is that they fire outside the React lifecycle. You set up a onSnapshot listener that updates state whenever Firestore changes — but you need to be careful about cleanup, stale closures, and avoiding re-subscription on every render.
My solution was a custom useTasks hook that encapsulates the listener, handles cleanup in the useEffect return, and exposes a clean API to the rest of the app. Components never touched Firebase directly.
useEffect(() => {
const unsubscribe = onSnapshot(tasksQuery, (snapshot) => {
const tasks = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}))
setTasks(tasks)
})
return () => unsubscribe()
}, [userId])
What I'd Do Differently
The analytics component ended up doing too much — fetching data, transforming it, and rendering charts all in one place. If I rebuilt it, I'd separate the data transformation into a utility function and keep the component purely presentational.
I'd also add offline support. Firebase has built-in offline persistence but I didn't enable it. For a task app where you might be on a plane, that's an obvious miss.
Key Takeaways
- TypeScript pays off most in complex state shapes — typed tasks, typed filters, typed chart data all made refactoring painless
- Firebase real-time listeners need careful lifecycle management in React — always clean up your subscriptions
- Mantine's first-party chart library saves significant integration time compared to mixing UI and chart libraries from different ecosystems