Building a modern todo app with Zustand, Firebase, and Next.js
A real-time todo app with authentication, CRUD operations, and predictable global state, built with Next.js, Firebase, and Zustand.
On this page
A todo app is the smallest app that still needs auth, persistence, real-time updates, and optimistic UI. That makes it a good place to get the architecture right.
Separate server state from UI state
This is the rule that keeps the codebase clean.
- Firebase holds the todos. That is server state.
- Zustand holds the filter, the draft text, and the selection. That is UI state.
Mixing them creates synchronisation bugs.
type UiState = {
filter: "all" | "active" | "done";
draft: string;
setFilter: (filter: UiState["filter"]) => void;
setDraft: (draft: string) => void;
};
export const useUiStore = create<UiState>((set) => ({
filter: "all",
draft: "",
setFilter: (filter) => set({ filter }),
setDraft: (draft) => set({ draft }),
}));
Subscribe to real-time updates
useEffect(() => {
const unsubscribe = onSnapshot(
query(collection(db, "todos"), orderBy("createdAt", "desc")),
(snapshot) => {
setTodos(snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() })));
},
);
return unsubscribe;
}, []);
Always return the unsubscribe function. This is the most common Firebase leak.
Optimistic updates that roll back
The UI should update before the network confirms, then correct itself on failure.
const previous = todos;
setTodos([optimistic, ...todos]);
try {
await addDoc(collection(db, "todos"), optimistic);
} catch {
setTodos(previous);
}
Accessible interactions
- Toggle with a real checkbox, not a styled div
- Announce changes with an
aria-liveregion - Keep delete behind an undo window
Optimistic UI is a promise. Make sure you can keep it.
Model server state and UI state separately, subscribe carefully, and the rest follows.
Continue reading
Related articles
How I built a fullstack authentication system with Next.js, Supabase, and Tailwind
Email sign-in, session handling, and protected routes in a Next.js App Router app, built on Supabase auth with a clean Tailwind interface.
How to create a responsive navbar in Next.js with TypeScript and Zustand
Build an accessible, responsive navigation bar in the Next.js App Router using TypeScript and Zustand for predictable global state.
Serverless file uploads with Supabase Storage and Next.js API routes
Secure, serverless file uploads in Next.js using Supabase Storage, with validation, size limits, and public access handling.