TaskFlow is a modern To-Do manager built with Next.js, TypeScript, Zustand, Tailwind CSS v4, and shadcn/ui components.
It focuses on a smooth UX with:
- Fast task toggling via optimistic updates
- Server-backed CRUD using
dummyjsonAPI - Clear separation between pending and completed tasks
- Search and pagination support
- App name:
TaskFlow - Main page: two task lists (
To DoandCompleted) - Includes: search, add task dialog, delete confirmation, loading skeletons, and toast feedback
- Framework: Next.js 16 (App Router)
- Language: TypeScript
- UI: Tailwind CSS v4 + shadcn/ui + Lucide icons
- State management: Zustand
- Testing: Vitest + Testing Library + jsdom
- Notifications: Sonner
- API:
https://dummyjson.com
- Node.js 20+
- pnpm (recommended)
pnpm installThis project reads the API base URL from NEXT_PUBLIC_API_URL.
.env.example already includes a default value:
NEXT_PUBLIC_API_URL=https://dummyjson.comCreate .env.local (if you do not already have one) and copy that value.
pnpm devOpen http://localhost:3000.
pnpm dev: start local dev serverpnpm build: production buildpnpm start: run production serverpnpm lint: run ESLintpnpm test: run tests in watch mode with Vitestpnpm test:run: run tests once (CI-friendly)
This project uses Vitest as the test runner, with Testing Library for component behavior and jsdom for a browser-like test environment.
- Run tests in watch mode:
pnpm test- Run tests once:
pnpm test:runCurrent test coverage focuses on store and API behavior under src/stores and src/lib/api.
src/
app/
layout.tsx # Root layout + toaster
page.tsx # Main page composition
components/
TodoInitializer.tsx # Initial data fetch on mount
CheckboxInTable.tsx # Main list rendering and states
AddToDoDialog.tsx # Create todo UI
ToDoItem.tsx # Single todo row + delete action
PaginationBar.tsx # Pagination controls
Searchbar.tsx # Search query input + result count
hooks/
useTodos.ts # Data orchestration + optimistic logic
stores/
todo.store.ts # Global Zustand store
lib/api/
todos.api.ts # API client functions
types/
index.ts # Shared types
TodoInitializertriggersfetchTodos(0)on first render.useTodoscallsgetTodos(limit, skip)fromtodos.api.ts.- Response is stored in Zustand (
todos,total,currentPage, flags). CheckboxInTablefilters by search term and splits tasks into pending/completed.PaginationBarrequests a new page throughgoToPage.
-
Create:
AddToDoDialogcallsaddTodo(todoText).- Success prepends the new todo in the local list.
- New IDs are tracked in
localIdsso local-only todos can be handled safely.
-
Toggle complete (optimistic):
- UI updates immediately via
updateTodoLocal. - If API call fails, state is reverted and error is shown.
- For local-only todos, no remote PATCH is attempted.
- UI updates immediately via
-
Delete:
- If todo is local-only, delete happens instantly in local store.
- Otherwise, API DELETE runs first, then local state is updated.
useState works well for local component state, but this app has state shared across many components:
- task list data
- pagination metadata
- loading/error/fetch flags
- search query
- dialog state
Using only useState would force prop drilling or duplicated logic across Header, CheckboxInTable, PaginationBar, dialogs, and list items.
useContext can centralize state, but large context objects often cause broad re-renders when any value changes. With a frequently updated todo list, this can impact responsiveness.
Zustand provides:
- A simple global store with minimal boilerplate
- Fine-grained state selection per component
- Clear action-style updates (
addTodoLocal,updateTodoLocal,deleteTodoLocal) - Easy access to current state in async logic (
useToDoStore.getState())
In short, Zustand keeps shared state predictable and scalable without the reducer/provider overhead of a full context architecture.
Optimistic updates are used on task toggle to improve perceived performance.
- Immediate UI feedback when checking/unchecking tasks
- Less "waiting" feel during network calls
- Better interaction flow for high-frequency actions
- Original state is known (
currentCompleted) - On API failure, the UI is rolled back to previous value
- Error state is set so the user is informed
This gives the speed of local interactions while keeping data integrity when the backend fails.
Base URL from NEXT_PUBLIC_API_URL:
GET /todos?limit={limit}&skip={skip}POST /todos/addPATCH /todos/{id}DELETE /todos/{id}
- Loading skeleton while fetching
- Empty state when no matching tasks exist
- Error with retry button on fetch failure
- Toast notifications for create/delete feedback
- GitHub:
https://github.com/developerleonardo/pt-taskflow-leonardo
