Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 79

Building a CRUD Task Manager UI

Recommended resources

Hello! Welcome back to the final capstone project.

In our last lesson, we successfully set up the navigational structure of our Task Manager using React Router. We have distinct pages for the dashboard, projects, and profile, all neatly wrapped in a shared layout with a sidebar. Our DashboardPage is currently a blank canvas, waiting to be brought to life.

Today, we will focus on that canvas. Our goal is to create the responsive and interactive user interface for creating, viewing,updating, and deleting tasks. We will build out the DashboardPage to be a fully functional, self-contained task manager.

To do this, we'll work entirely on the frontend for now, managing our tasks in React's local state. This is a common and highly effective development practice: build and perfect the UI in isolation before connecting it to a live backend.

By the end of this lesson, you will have a UI that looks and feels like a complete application, ready for us to plug into our backend API in the next step.

Modern React CRUD Task List UI
This image shows the kind of interactive UI we're aiming for. It has an input for adding tasks, and each task in the list has controls for updating (like the checkbox and edit icon) and deleting.

Part 1: Structuring the UI with Components and State

First, we need a plan. A complex UI like a task manager is best built by breaking it down into smaller, reusable components. For our DashboardPage, a good structure would be:

  • DashboardPage.jsx: The main container component. It will own the state (the list of tasks) and the logic (functions to add, update, delete tasks).
  • A form for adding tasks: This will be part of the DashboardPage's JSX. It will include a text input and a submit button.
  • A task item component: A separate component to represent a single task in the list. It will display the task's text and have its own controls, like a "complete" checkbox and a "delete" button.

The core of our application will be the list of tasks. We'll manage this list using the useState hook.

// Inside DashboardPage.jsx
const [tasks, setTasks] = useState([]);

This tasks array will hold task objects. A typical task object might look like this:
{ id: 1, text: 'Build the task UI', completed: false }


Part 2: A Step-by-Step Video Guide

Building a to-do list is a classic React exercise, and watching an expert build one is an excellent way to learn the patterns. The following video provides a complete walkthrough that aligns perfectly with our goal for today. It uses React, Tailwind CSS for styling, and covers all the CRUD (Create, Read, Update, Delete) operations we need.

We will use this video as our primary guide for the implementation. I'll break it down into logical steps. As you follow along, focus on understanding the why behind each piece of code.

How To Make To-Do List App Using React JS And Tailwind CSS | Create Task App In React JS

The 'How To Make To-Do List App' tutorial by GreatStack is a fantastic, practical guide. We will use it to build our task management UI. The video author builds it as a standalone app, but you'll be implementing this logic inside your existing DashboardPage.jsx.

You can watch this video from the start up to 01:02:06. I've broken down the key segments below so you can focus on one piece of functionality at a time.

Follow the sections of the video to implement the functionality. I've provided explanations and code snippets to clarify the key concepts.

Step 2.1: Building the UI Structure and Rendering Tasks (Read)

First, let's get the basic UI on the screen and render a static list. This corresponds to the Read part of CRUD.

Focus in the video: 02:41 to 32:53

This part of the video covers:

  • Setting up the main component structure (Todo.jsx and TodoItems.jsx in the video, which you can adapt into your DashboardPage.jsx and a new TaskItem.jsx component).
  • Styling the components with Tailwind CSS to create the input box, button, and task list area.
  • Creating the basic JSX for an individual task item.

The key pattern for rendering the list is using the .map() method on your state array:

// Inside the return statement of DashboardPage.jsx
<div className="task-list">
  {tasks.map((task) => (
    <TaskItem 
      key={task.id} 
      task={task} 
    />
  ))}
</div>

Step 2.2: Adding New Tasks (Create)

Now, let's make the input form functional.

Focus in the video: 34:43 to 46:04

The logic for adding a task is as follows:

  1. Capture the user's input from the text field. The video uses useRef, which is a valid approach. Another common way is to use a separate piece of state: const [newTaskText, setNewTaskText] = useState('');.
  2. When the "Add" button is clicked, create a new task object. It's crucial to give it a unique id. The video uses Date.now(), which is simple and effective for client-side state.
  3. Update the tasks state array by creating a new array that includes all the old tasks plus the new one. This is a fundamental concept in React: never mutate state directly.
// A function inside DashboardPage.jsx
const addTask = (text) => {
  if (!text.trim()) return; // Don't add empty tasks

  const newTask = {
    id: Date.now(), // Simple unique ID
    text: text,
    completed: false,
  };

  setTasks([...tasks, newTask]); // Use the spread operator to create a new array
};
Test your understanding!

Why is it important to use setTasks([...tasks, newTask]) instead of tasks.push(newTask) followed by setTasks(tasks)?

Show answer

React determines whether to re-render a component by checking if its state or props have changed. When you use array methods that mutate the array in place, like .push(), the reference to the tasks array in memory remains the same. React performs a shallow comparison and concludes that nothing has changed, so it doesn't trigger a re-render.

By using the spread operator [...tasks, newTask], you are creating a brand new array with a new reference in memory. When you pass this new array to setTasks, React sees that the state has changed and correctly re-renders the component to display the new task.

Step 2.3: Deleting Tasks (Delete)

Next, let's implement the delete functionality.

Focus in the video: 46:56 to 51:49

The pattern here is called "lifting state up". The state lives in DashboardPage, but the delete action is triggered from a button in TaskItem.

  1. Create a deleteTask function in DashboardPage that takes an id as an argument.
  2. This function will use the .filter() method to create a new array that excludes the task with the matching id.
  3. Pass the deleteTask function down to each TaskItem as a prop.
  4. In TaskItem, call the function from the onClick handler of the delete button, passing its own task.id.
// A function inside DashboardPage.jsx
const deleteTask = (id) => {
  setTasks(tasks.filter(task => task.id !== id));
};

// In the .map() method in DashboardPage.jsx
<TaskItem 
  key={task.id} 
  task={task}
  onDelete={deleteTask} // Pass the function as a prop
/>

// Inside TaskItem.jsx
<button onClick={() => onDelete(task.id)}>Delete</button>

Step 2.4: Updating Tasks (Update)

Finally, let's allow users to mark tasks as complete.

Focus in the video: 51:49 to 01:02:06

This follows the exact same pattern as deleting.

  1. Create a toggleComplete function in DashboardPage that takes an id.
  2. This function uses .map() to create a new array. For each task, it checks if the id matches. If it does, it returns a new task object with the completed property flipped. If not, it returns the original task.
  3. Pass this toggleComplete function down to TaskItem as a prop.
  4. In TaskItem, use this function in an onClick handler (e.g., on the checkbox or the task text).
// A function inside DashboardPage.jsx
const toggleComplete = (id) => {
  setTasks(
    tasks.map(task => 
      task.id === id ? { ...task, completed: !task.completed } : task
    )
  );
};

This function is a great example of immutable updates. It doesn't change any of the original objects; it builds a completely new array with one new, updated object in it.


Part 3: Consolidating with Code Examples

The video is a great visual guide. The article below provides concise code snippets for these same CRUD functions, which can be very helpful for reference. This article implements them as async functions preparing for API calls, which is exactly what we will do in the next lesson. For today, you can ignore the async/await and fetch parts and focus on the state manipulation logic (setTodos).

React sample project: Task Manager

The 'React sample project: Task Manager' article from Beeceptor has excellent, production-ready examples of the handler functions we need. Use it as a reference for the logic.

Review 'Step 6: Add Task Creation', 'Step 7: Implement Task Updates', and 'Step 8: Add Task Deletion'. Pay attention to the setTodos(...) calls to see how the state is updated for each action. Also, look at 'Step 10: Build the Complete UI' for a complete JSX example that puts it all together.


Conclusion

Congratulations! You have now built a responsive and fully interactive task management UI within your DashboardPage. While it currently only stores data in local state (which is lost on refresh unless you implemented the optional localStorage step from the video), you have all the necessary components, state, and logic in place.

Key Takeaways:

  • You structured a complex UI by breaking it down into smaller, manageable components.
  • You managed the application's core data (the list of tasks) using the useState hook.
  • You implemented all four CRUD operations on the frontend using immutable state update patterns (map, filter, spread syntax).
  • You practiced the "lifting state up" pattern by passing handler functions as props from a parent component (DashboardPage) to a child (TaskItem).

Preview of the Next Lesson:
Our UI is functional but isolated. In the next lesson, we will bridge the gap between our frontend and backend. We will replace our local state management with live data from our server by wiring up our addTask, deleteTask, and toggleComplete functions to the REST API endpoints we built earlier. This will make our Task Manager a true full-stack application.