Capstone - Task Management

By Syncfusion®, Inc. · Last updated

Build a complete Task Manager app step by step: setup, reusable components, routing, shared state with context and a reducer, validated forms, an API service, error handling and performance.

What is a Capstone Project?

A capstone project is one bigger project that joins together the skills from a whole course. Instead of small examples, you build one complete app.

In this lesson you build a Task Manager for the Book Corner team. The team can see tasks, add new ones, mark them done, delete them, filter them and see simple stats. The tasks are saved through an API.

Real-World Example

A small team needs one app to track its work. Each person opens /tasks, adds a task with a title and a priority, and marks it done when finished.

The data is saved through a REST API, so everyone sees the same list after a refresh. This capstone builds that app and uses every skill from the course:

  • TypeScript types for a task (Lesson 2)
  • Reusable components with props (Lessons 3–4)
  • Routing with three pages (Lesson 16)
  • Context and a reducer for shared state (Lessons 14–15)
  • Forms and validation (Lessons 9–10)
  • The Fetch API with a service layer (Lesson 22)
  • Error boundaries, memo and lazy loading (Lessons 24–25)

The capstone is built in six steps, each one using skills from earlier chapters

1 Setup types

2 Parts components

3 Pages routing

4 State context

5 Forms validation

6 API fetch

each step adds one layer, and the app works after every step

Build a little, run it, then add the next part.

Project Setup and Application Structure

On your own computer, you create the project with Vite and add React Router:

npm create vite@latest task-manager -- --template react-ts
cd task-manager
npm install react-router-dom
npm run dev

Then you plan the folders. Each folder has one job, so files are easy to find.

A typical folder structure for the Task Manager

src/ types.ts components/ Button.tsx TaskCard.tsx pages/ TaskListPage.tsx ... context/ TasksContext.tsx services/ taskService.ts types: the shape of a task components: small reusable parts pages: one per route context: shared state services: talk to the API

Each folder has one job. The playground keeps files flat, but the roles are the same.

The first file to write is types.ts. It says what a task looks like. Every other file uses these types.

  • Priority can only be "low", "medium" or "high".
  • NewTask is Omit<Task, "id">: a task without an id. The server gives the id when it saves the task.

Try changing priority: "high" to priority: "urgent" in the lab. The editor marks it red, because "urgent" is not a Priority. Types catch mistakes before you run the app.

Building Reusable Components

Next, split the screen into small parts that you can use again:

  • Button — one button style for the whole app. The variant prop picks the colour: variant = "plain" is the default value.
  • PriorityBadge — a coloured label. className={"badge " + priority} gives classes like badge high.
  • TaskCard — one task row, with a checkbox, the title, the badge and a Delete button.

TaskCard does not change any data itself. It receives the task and two functions, onToggle and onRemove, and calls them. The parent decides what happens. This keeps the card simple and reusable.

Implementing Routing and Navigation

The app has three pages. React Router shows the right page for each URL.

The Task Manager has three pages, each with its own URL

/ TaskListPage

/new NewTaskPage

/stats StatsPage (lazy)

The nav bar links to each URL. A * route catches everything else.

In the lab below:

  • <NavLink to="/"> makes a link. NavLink adds the class active to the link of the current page, so the CSS can highlight it. end means "only for exactly /".
  • <Route path="/tasks/:id"> has a URL parameter. In TaskDetailPage, const { id } = useParams(); reads it.
  • <Route path="*"> catches any unknown URL and shows "Page not found".

Click a task title, then use the address bar's Back button.

Managing State Across the Application

Many parts of the app need the task list: the header shows how many tasks are left, the list page shows them, and the stats page counts them. Passing the list through props to all of them would be messy.

So the tasks live in a context, managed by a reducer. Open the TasksContext.tsx tab:

  • tasksReducer is the only place that changes the list. Each case handles one action: "added", "toggled" or "removed".
  • TasksProvider wraps the app and shares the tasks and three functions: addTask, toggleTask, removeTask.
  • useTasks() is a small custom hook. Any component calls it to get the tasks. It throws a clear error if you forget the provider.

Data flows from the API through the service and the context to the pages and components

API /tasks

taskService fetch

TasksContext state

Pages components

user actions travel back the other way

Pages never call fetch. They ask the context, which asks the service.

Tick a checkbox. The header count changes too, because Header and TaskList read the same context.

Creating, Validating, and Managing Tasks

Now users can add their own tasks. TaskForm is a controlled form: the input's value comes from state.

Before saving, validateTitle checks the title and returns an error message, or "" when all is well:

  • if (text === "") return "Please enter a title."; — the title cannot be empty.
  • if (text.length < 3) return "Title must be at least 3 characters."; — too short.

validateTitle is a plain function outside the component, so it is easy to read and to test.

The list also gets filters: All, Active and Done. The filter is local state in the list, because no other component needs it.

Try to add "Hi". You see the error message. Then add "Call the publisher".

Working with APIs

So far, tasks live only in memory. A real app saves them on a server. The playground has a pretend API at https://api.bookcorner.dev/tasks that supports GET, POST, PUT and DELETE.

All the fetch code lives in one file, taskService.ts:

  • request<T> is one helper for every call. It sends JSON, checks response.ok, and throws an error when the server fails.
  • taskService.getAll(), create(), update() and remove() each make one request.

The context now calls the service and keeps loading and error in its state. Components do not change at all: they still call useTasks(). That is the power of the layers.

Watch the console. Each action prints a line like [API] PUT /tasks/1 → 200. The pretend server starts fresh on every Run, just like a test database.

Handling Errors, Optimizing Performance, and Final Review

The last step joins everything into the finished app, and adds the finishing touches from Chapter 9:

  • Errors. The context catches failed requests and stores a friendly message. An ErrorBoundary around the pages shows a fallback if a page crashes, while the header and nav keep working.
  • Performance. TaskCard is wrapped in memo. The context functions use useCallback, so they stay the same and memo can skip cards that did not change. The filtered list uses useMemo.
  • Loading. StatsPage is loaded with lazy, inside Suspense, so its code downloads only when someone opens it.

Here is the complete Task Manager. Open every tab and follow how the parts connect.

Final review checklist

Before you call a project finished, check:

  • It works. Add, tick, delete and filter tasks. Visit every page, and an unknown URL.
  • Bad input is handled. Empty and short titles show a clear message.
  • Failures are handled. Loading and error messages show. A crash in one page does not blank the whole app.
  • The code is tidy. Each file has one job. Types are used everywhere. No API URL is copied into components.
  • It is fast enough. No large wasted re-renders, and rare pages load lazily.

Easy Way to Remember: types first, then parts, pages, state, forms, API, and finally polish.

Ideas to go further

  • Add a due date to each task and sort by it.
  • Add an edit page at /tasks/:id/edit.
  • Add a login page and protect the pages, as in the User Access Control lesson.

Download Project Files

The complete Task Manager is ready as a real Vite + React + TypeScript project. It has the same code as the final lab, split into a realistic folder structure, plus a small local API that saves your tasks in a file.

Download Capstone Project: Download Task Management Project

What is inside

task-manager/
  package.json          scripts and dependencies
  vite.config.ts        Vite settings
  index.html            the page with <div id="root">
  .env                  VITE_API_URL=http://localhost:3001
  db.json               the tasks, saved by the local API
  src/
    main.tsx            createRoot + render
    App.tsx             providers, router and routes
    types/task.ts       the Task type
    components/         Button, PriorityBadge, TaskCard, TaskForm, Header, NavBar, ErrorBoundary
    pages/              TaskListPage, NewTaskPage, TaskDetailPage, StatsPage, NotFoundPage
    context/            TasksContext (useReducer + useTasks)
    services/           apiClient + taskService (all fetch calls)
    utils/              validateTask.ts

How to download, set up and run

  1. Install Node.js 18 or newer from nodejs.org, if you do not have it yet.
  2. Download the ZIP file with the link above.
  3. Unzip it. You get a folder called task-manager.
  4. Open a terminal in that folder (in VS Code: File, Open Folder, then Terminal, New Terminal).
  5. Install the packages:
npm install
  1. Start the project:
npm run dev

npm run dev starts two things together: the React app at http://localhost:5173 (it opens in your browser), and a small REST API at http://localhost:3001/tasks, made with json-server. Press Ctrl + C in the terminal to stop both.

Your tasks are saved in db.json. Open it while the app runs and watch it change when you add, tick or delete a task. To start again, change it back to the three sample tasks.

Common mistakes

MistakeWhat you seeFix
Calling fetch inside many componentsThe same URL in many files, hard to changeKeep requests in taskService.ts
Using useTasks() outside TasksProviderError: useTasks must be used inside TasksProviderWrap the app in <TasksProvider>
Changing the task array directly with pushThe screen does not updateReturn a new array from the reducer
Saving a form without validationEmpty tasks in the listCheck the title with validateTitle first
Forgetting Suspense around a lazy pageAn error when the page opensWrap Routes in <Suspense fallback={...}>
Building everything at onceMany bugs, hard to findBuild one step, run it, then add the next

Check Yourself

Exercises

What to Remember

  • A capstone joins every skill into one app. Build it in small steps and run it after each one.
  • Types first, then reusable components, routes, shared state in a context with a reducer, validated forms, and an API service.
  • Finish with error handling, memo/useCallback/lazy where they help, and a final review checklist.

Next: Cheat Sheet