Tangy is building your next step
Shaping the lesson around what you want to make.
Tangy is building your next step
Shaping the lesson around what you want to make.
Computer Science
You’ll build a small working to-do list from scratch: an Express server delivers the page and handles a tiny API, while browser JavaScript lets you add, complete, and remove tasks without reloading. You already have a foundation in HTML structure, CSS selectors, and guided React components, so this course uses those skills directly while introducing the server-and-browser connection you need for this focused project.
Follow the path module by module. The layout keeps the lesson count, your progress, and the module description in one scan.
Create the standalone to-do interface
3 lessonsBuild the visible application shell first, using the learner’s existing HTML and CSS strengths. By the end, the project is a usable static interface with task rows, controls, and an empty state before any server work begins.
Put the interface behind an Express server
3 lessonsMove from a browser-only prototype to a one-process Node.js application. The learner adds Express gradually, keeps the familiar static page working, and exposes a tiny in-memory API that the browser can call.
Finish the task lifecycle and polish the demo
3 lessonsComplete the core CRUD behavior and make the finished project reliable to show. The learner adds completion and deletion endpoints, handles ordinary failure cases, and verifies the whole flow from a clean start.
Public lesson
This lesson gives the to-do app a clear structure before adding behavior. You will decide what one task looks like, then use that shape to build a page with:
The goal is not just to make the page look like a to-do app. It is to make the page’s structure communicate its meaning to the browser, assistive technology, and your future React code.
Your HTML structure is already a strength, so we will use that carefully while keeping the React part small and explicit.
Predict
What will happen?
Before opening the file, think about this question:
If someone could not see the styling, which elements would still tell them what this page is?
A reasonable outline is:
main
├── h1
├── form
│ ├── label
│ ├── input
│ └── button
└── section
├── heading
└── list or empty-state message
main
├── h1
├── form
│ ├── label
│ ├── input
│ └── button
└── section
├── heading
└── list or empty-state message
The important distinction is between appearance and meaning:
div can group content, but it does not explain what the group is.form tells the browser that the user is entering information to submit.ul tells the browser that several items belong to one list.label tells the user what an input is for.This structure will still matter later when you add state, filtering, and event handlers. If the shell is vague now, those features have to work around that vagueness.
Tasks
Open the existing React page or component that is intended for this lesson. If you are unsure which file to edit, inspect the project’s existing page/component files and follow the project’s documented development command rather than creating a new project.
Start with the smallest useful task shape. A task needs:
const exampleTask = {
id: /* stable identifier */,
title: /* text shown to the user */,
completed: /* true or false */
};
const exampleTask = {
id: /* stable identifier */,
title: /* text shown to the user */,
completed: /* true or false */
};
Use these meanings:
id identifies the task independently of its position in the array.title is the user-facing task text.completed describes whether the task is finished.The id matters because a task’s position can change when tasks are sorted, deleted, or filtered. React also needs a stable identity when rendering a list. The title should contain the task’s content, not markup. The completed value should be a boolean, not a string such as "false".
Now create a small collection using your own task text. Keep it to one or two objects for now:
const initialTasks = [
{
id: /* your first id */,
title: /* your first task title */,
completed: false
}
// Add a second task only if it helps you inspect the list layout.
];
const initialTasks = [
{
id: /* your first id */,
title: /* your first task title */,
completed: false
}
// Add a second task only if it helps you inspect the list layout.
];
Do not add fields such as priority, dueDate, or createdBy yet. Extra fields create decisions that the page does not use. A small shape is easier to understand and easier to change.
Pause and inspect your data. Explain to yourself:
If you cannot answer one of these, adjust the object before building the page.
Tasks
In the relevant component, replace or extend the current page markup with a semantic outer structure. Use a skeleton like this, but choose the exact text and class names yourself:
return (
<main className="todo-page">
<header>
<h1>{/* page title */}</h1>
<p>{/* short description of what the page does */}</p>
</header>
{/* task form goes here */}
<section aria-labelledby="task-list-heading">
<h2 id="task-list-heading">{/* task section heading */}</h2>
{/* task list or empty state goes here */}
</section>
</main>
);
return (
<main className="todo-page">
<header>
<h1>{/* page title */}</h1>
<p>{/* short description of what the page does */}</p>
</header>
{/* task form goes here */}
<section aria-labelledby="task-list-heading">
<h2 id="task-list-heading">{/* task section heading */}</h2>
{/* task list or empty state goes here */}
</section>
</main>
);
The aria-labelledby connection is deliberate. It tells assistive technology that the section’s accessible name comes from the h2 with that ID. The visible heading and the accessibility information stay synchronized instead of describing the section twice.
Use a header for introductory content, but do not put every group inside a header. The main element should represent the page’s primary content, and the section should represent the distinct task area.
Run the project using its documented command and open the page in the browser.
Then inspect the page with developer tools:
main region for this page?h1?If the page is blank or the compiler reports an error, read the first error from the top. JSX errors often cause later lines to appear broken even though the real problem is earlier.
Tasks
A form should describe the user’s action, even before submission works. Add the form inside the page shell:
<form>
<label htmlFor="task-title">
{/* label text */}
</label>
<input
id="task-title"
name="task-title"
type="text"
placeholder={/* short example, if useful */}
/>
<button type="submit">
{/* action text */}
</button>
</form>
<form>
<label htmlFor="task-title">
{/* label text */}
</label>
<input
id="task-title"
name="task-title"
type="text"
placeholder={/* short example, if useful */}
/>
<button type="submit">
{/* action text */}
</button>
</form>
Adapt the missing values to your page.
Three details are easy to overlook:
htmlFor must match the input’s id. In JSX, use htmlFor, not HTML’s for.name gives the field a meaningful form control name.type="submit" makes the button’s purpose explicit.Do not add a submit handler yet unless the existing project requires one to prevent a page reload. At this stage, the form’s job is to establish the interface and its relationships, not to pretend that task creation is complete.
A placeholder is not a label. A placeholder disappears when the user types and may be difficult to read. Keep the visible label.
Use the browser to test the form structurally:
Tab. Does focus reach the input and then the button in a sensible order?The last observation is useful even if you do not implement validation yet. The browser may submit the form or report no constraint; that behavior is a reminder that visual markup and interaction behavior are separate concerns.
Tasks
The task area should represent a collection as a real list. Use the existing initialTasks array rather than writing unrelated markup for every task.
A partial rendering shape could look like this:
<ul className="task-list">
{initialTasks.map((task) => (
<li key={/* stable task identifier */}>
<label>
<input
type="checkbox"
checked={/* task completion value */}
readOnly
/>
<span>{/* task title */}</span>
</label>
</li>
))}
</ul>
<ul className="task-list">
{initialTasks.map((task) => (
<li key={/* stable task identifier */}>
<label>
<input
type="checkbox"
checked={/* task completion value */}
readOnly
/>
<span>{/* task title */}</span>
</label>
</li>
))}
</ul>
Fill in the expressions using the properties of your task object.
Notice the relationship:
.map() turns each data object into one list item.key gives React a stable identity for that list item.completed.title.readOnly is appropriate for this static shell if the checkbox is controlled by a value but does not yet have a change handler. Without it, React may warn that the field is controlled but cannot be changed. You are not implementing completion behavior in this lesson; you are making the relationship visible.
Do not use the array index as the key just because it is available. If a task is removed later, the remaining tasks can shift positions, and index keys no longer represent the same task.
With one or two tasks in initialTasks, verify:
lititlecompleted: true starts checkedChange one task’s completed value and reload. The visible checkbox should change without changing the rendering code. That demonstrates that the page is using the data shape rather than hardcoded task markup.
Tasks
A task list also needs to explain what is happening when the collection is empty. Otherwise the user may see a heading followed by blank space and wonder whether the page failed.
Render either the list or an empty-state message based on the number of tasks:
{initialTasks.length > 0 ? (
<ul className="task-list">
{/* map the tasks here */}
</ul>
) : (
<p className="empty-state">
{/* explain that there are currently no tasks */}
</p>
)}
{initialTasks.length > 0 ? (
<ul className="task-list">
{/* map the tasks here */}
</ul>
) : (
<p className="empty-state">
{/* explain that there are currently no tasks */}
</p>
)}
The condition is based on the collection, not on a separate hasTasks value. That avoids two pieces of information drifting apart.
Temporarily change your data to:
const initialTasks = [];
const initialTasks = [];
Do not leave the list’s old static item outside the conditional. If you do, the empty state and the task item will appear together, which makes the page contradict itself.
Reload with an empty array and confirm that:
Then restore one task and confirm that the list returns. This is an important check: both branches should be valid page states, not just one successful screenshot.
Explain it back
Put it in your own words
Your component should now have the following conceptual shape:
main
├── header
│ ├── h1
│ └── description
├── form
│ ├── label + input
│ └── submit button
└── section
├── h2
└── one of:
├── ul > li for each task
└── empty-state paragraph
main
├── header
│ ├── h1
│ └── description
├── form
│ ├── label + input
│ └── submit button
└── section
├── h2
└── one of:
├── ul > li for each task
└── empty-state paragraph
Use the browser and the DOM inspector to verify the actual elements, not just how they look.
Finally, explain the page in your own words:
key={task.id} rather than the array index?If the answers are clear, the shell is doing more than displaying text: it has established a reliable contract between the task data, the React render, and the page’s semantic structure.
20 more characters
3 modules · 9 lessons
Create the standalone to-do interface
Put the interface behind an Express server
Finish the task lifecycle and polish the demo
Learn by building your own version.
Remix this public project to open the workspace, follow the guided build, and let the AI mentor teach you through the work instead of doing it for you.