Skip to main content

Project Overview

The Project Management app is a practice project from Section 9 that demonstrates how to build a full-featured application for managing projects and their associated tasks. This project focuses on advanced React patterns including refs, portals, and complex state management.
Project Management App Interface

Learning Objectives

Refs & ForwardRef

Access DOM elements and manage component references

React Portals

Render modals outside the component hierarchy

State Management

Handle complex application state patterns

Form Validation

Validate user input with refs

Key Features

Project Management

Users can add new projects with title, description, and due date
function handleAddProject(projectData) {
  setProjectsState((prevState) => {
    const projectId = Math.random();
    const newProject = {
      ...projectData,
      id: projectId,
    };

    return {
      ...prevState,
      selectedProjectId: undefined,
      projects: [...prevState.projects, newProject],
    };
  });
}
Click on a project in the sidebar to view its details and tasks
function handleSelectProject(id) {
  setProjectsState((prevState) => {
    return {
      ...prevState,
      selectedProjectId: id,
    };
  });
}

const selectedProject = projectsState.projects.find(
  (project) => project.id === projectsState.selectedProjectId
);
Remove projects and their associated tasks from the application
function handleDeleteProject() {
  setProjectsState((prevState) => {
    return {
      ...prevState,
      selectedProjectId: undefined,
      projects: prevState.projects.filter(
        (project) => project.id !== prevState.selectedProjectId
      ),
    };
  });
}

Task Management

Each project can have multiple tasks that can be added and removed:
Tasks.jsx
import NewTask from './NewTask.jsx';

export default function Tasks({ tasks, onAdd, onDelete }) {
  return (
    <section>
      <h2 className="text-2xl font-bold text-stone-700 mb-4">Tasks</h2>
      <NewTask onAdd={onAdd} />
      {tasks.length === 0 && (
        <p className="text-stone-800 my-4">
          This project does not have any tasks yet.
        </p>
      )}
      {tasks.length > 0 && (
        <ul className="p-4 mt-8 rounded-md bg-stone-100">
          {tasks.map((task) => (
            <li key={task.id} className="flex justify-between my-4">
              <span>{task.text}</span>
              <button
                className="text-stone-700 hover:text-red-500"
                onClick={() => onDelete(task.id)}
              >
                Clear
              </button>
            </li>
          ))}
        </ul>
      )}
    </section>
  );
}

Core Concepts

Using Refs for Form Handling

The project uses refs to access form input values without controlled components:
NewProject.jsx
import { useRef } from 'react';
import Input from './Input.jsx';
import Modal from './Modal.jsx';

export default function NewProject({ onAdd, onCancel }) {
  const modal = useRef();
  const title = useRef();
  const description = useRef();
  const dueDate = useRef();

  function handleSave() {
    const enteredTitle = title.current.value;
    const enteredDescription = description.current.value;
    const enteredDueDate = dueDate.current.value;

    // Validation
    if (
      enteredTitle.trim() === '' ||
      enteredDescription.trim() === '' ||
      enteredDueDate.trim() === ''
    ) {
      modal.current.open();
      return;
    }

    onAdd({
      title: enteredTitle,
      description: enteredDescription,
      dueDate: enteredDueDate,
    });
  }

  return (
    <>
      <Modal ref={modal} buttonCaption="Okay">
        <h2 className="text-xl font-bold text-stone-700 my-4">Invalid Input</h2>
        <p className="text-stone-600 mb-4">
          Oops ... looks like you forgot to enter a value.
        </p>
      </Modal>
      <div className="w-[35rem] mt-16">
        {/* Form content */}
        <Input type="text" ref={title} label="Title" />
        <Input ref={description} label="Description" textarea />
        <Input type="date" ref={dueDate} label="Due Date" />
      </div>
    </>
  );
}
When using refs with custom components, you must forward the ref using forwardRef.
The modal component demonstrates advanced patterns:
Modal.jsx
import { forwardRef, useImperativeHandle, useRef } from 'react';
import { createPortal } from 'react-dom';
import Button from './Button.jsx';

const Modal = forwardRef(function Modal({ children, buttonCaption }, ref) {
  const dialog = useRef();

  // Expose custom methods to parent components
  useImperativeHandle(ref, () => {
    return {
      open() {
        dialog.current.showModal();
      },
    };
  });

  // Render modal outside the component tree using portals
  return createPortal(
    <dialog
      ref={dialog}
      className="backdrop:bg-stone-900/90 p-4 rounded-md shadow-md"
    >
      {children}
      <form method="dialog" className="mt-4 text-right">
        <Button>{buttonCaption}</Button>
      </form>
    </dialog>,
    document.getElementById('modal-root')
  );
});

export default Modal;
React Portals allow you to render components outside their parent DOM hierarchy, perfect for modals and tooltips.

Application State Structure

The entire application state is managed in a single object:
App.jsx
const [projectsState, setProjectsState] = useState({
  selectedProjectId: undefined,  // undefined = no selection
                                  // null = adding new project
                                  // id = selected project
  projects: [],                   // Array of project objects
  tasks: [],                      // Array of all tasks
});
  • selectedProjectId: undefined - Show “no project selected” screen
  • selectedProjectId: null - Show “add new project” form
  • selectedProjectId: <id> - Show selected project details

Project Structure

09-practice-project-management/
├── src/
   ├── components/
   ├── Button.jsx              # Reusable button component
   ├── Input.jsx               # Forwarded ref input component
   ├── Modal.jsx               # Portal-based modal dialog
   ├── NewProject.jsx          # Project creation form
   ├── NewTask.jsx             # Task input component
   ├── NoProjectSelected.jsx   # Empty state component
   ├── ProjectsSidebar.jsx     # Project list sidebar
   ├── SelectedProject.jsx     # Project details view
   └── Tasks.jsx               # Task list component
   ├── App.jsx                     # Main app with state management
   └── main.jsx                    # Entry point
├── index.html                      # Includes modal-root div
└── tailwind.config.js              # Tailwind CSS configuration

Implementation Steps

The project is built incrementally across multiple videos:
1

Setup and Sidebar

Create the basic layout and style the projects sidebar
2

Component Structure

Build reusable Button and Input components with proper ref forwarding
3

State Management

Implement state logic to switch between different views
4

Project Creation

Handle form submission and add projects to state
5

Validation Modal

Add modal with portals for form validation feedback
6

Project Selection

Make projects selectable and display details
7

Project Deletion

Add ability to delete projects
8

Task Management

Implement task addition and deletion functionality

Key Patterns & Techniques

ForwardRef Pattern

Input.jsx
import { forwardRef } from 'react';

const Input = forwardRef(function Input({ label, textarea, ...props }, ref) {
  const classes = "w-full p-1 border-b-2 rounded-sm border-stone-300";
  
  return (
    <p className="flex flex-col gap-1 my-4">
      <label className="text-sm font-bold uppercase text-stone-500">
        {label}
      </label>
      {textarea ? (
        <textarea ref={ref} className={classes} {...props} />
      ) : (
        <input ref={ref} className={classes} {...props} />
      )}
    </p>
  );
});

export default Input;

Conditional Rendering

let content = (
  <SelectedProject
    project={selectedProject}
    onDelete={handleDeleteProject}
    onAddTask={handleAddTask}
    onDeleteTask={handleDeleteTask}
    tasks={projectsState.tasks}
  />
);

if (projectsState.selectedProjectId === null) {
  content = (
    <NewProject onAdd={handleAddProject} onCancel={handleCancelAddProject} />
  );
} else if (projectsState.selectedProjectId === undefined) {
  content = <NoProjectSelected onStartAddProject={handleStartAddProject} />;
}

Styling with Tailwind CSS

The project uses Tailwind CSS for styling:
export default function Button({ children, ...props }) {
  return (
    <button
      className="px-4 py-2 text-xs md:text-base rounded-md bg-stone-700 text-stone-400 hover:bg-stone-600 hover:text-stone-100"
      {...props}
    >
      {children}
    </button>
  );
}

Common Challenges

Challenge: Keeping projects and tasks synchronizedSolution: Store all data in a single state object and update immutably:
setProjectsState((prevState) => {
  return {
    ...prevState,
    tasks: [newTask, ...prevState.tasks],
  };
});
Challenge: Refs not working with custom componentsSolution: Wrap components with forwardRef and pass the ref to the actual DOM element

Testing Your Implementation

Next Steps

React Quiz Project

Learn useEffect and side effects

Advanced State

Explore Context API and useReducer

Build docs developers (and LLMs) love