The Real Web App
Advanced · 120 min build · 🏆 milestone project
You are going to build The Real Web App: a framework-free task board that keeps state in one object, renders the DOM from that state, handles events with delegation, loads starter data with fetch, and treats storage as a capability instead of a guarantee.
This is the browser project that proves you understand what frameworks do for you. The live workspace below is a starter app, not a finished solution: it runs cleanly, demonstrates the architecture, and leaves the core product work for you to complete.
What you'll build
You will build a task board with five jobs:
- a single
stateobject that owns tasks, filters, network state, and storage state - a
render(state)function that rebuilds the visible board from state - event delegation for task actions and filter buttons
- one live JSONPlaceholder fetch with loading, success, and failure UI
- an honest storage adapter that uses memory in this sandbox and shows how a normal same-origin page would persist
The app should feel small but real. It has user input, API data, failure states, task actions, progress checks, and a place to polish the interface.
Checkpoint 1: own the state and render loop
Start by reading the scaffold before changing it. Your first job is to make the board's truth easy to inspect.
Acceptance criteria:
statecontains the task list, active filter, network state, storage state, and completion row.render(state)is the only place that rebuilds task cards, counts, selected filters, empty messages, and the debug preview.- Event handlers update state first, then call the render path.
- Derived values like open count are computed from
state.tasks, not stored separately. - The page runs cleanly before and after your changes.
Checkpoint 2: finish event delegation
The starter already delegates task clicks from #task-list and filter clicks from .toolbar. Finish the product behavior so the board feels like one app instead of separate buttons.
Acceptance criteria:
- Clicking Done or Reopen toggles the matching task through state.
- Clicking Delete removes the matching task from state instead of only showing the starter TODO message.
- Filter buttons show All, Open, Done, and API tasks without duplicating list-rendering code.
- Adding a local task works from the button and the Enter key.
- No task action depends on a listener attached to each individual card.
Checkpoint 3: make fetch and failure states first-class
The Load sample tasks button uses JSONPlaceholder. Keep that live API path, then make the data flow more app-like.
Acceptance criteria:
- Fetch requests go only to
https://jsonplaceholder.typicode.com. - The UI shows loading while the request is in flight.
- The code checks
response.okbefore treating a response as success. - Success renders imported tasks with a visible
apisource. - Network failure, offline use, or an HTTP error leaves local tasks usable and shows a clear error state.
Checkpoint 4: handle storage honestly
The preview is sandboxed, so Web Storage and IndexedDB are not available here. The scaffold detects that and falls back to memory for the current Run.
Acceptance criteria:
- Storage access stays inside a small adapter or helper.
- The live project never claims memory fallback survives a new Run.
- Saving a snapshot updates a visible storage status.
- Your normal-page persistence plan stores only the app snapshot you actually need.
- Broken or missing saved data should not crash the app.
A normal same-origin page can persist this kind of snapshot with the same adapter boundary:
const STORAGE_KEY = "task-board:v1";
function saveBoardSnapshot(snapshot) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
}
function loadBoardSnapshot() {
const text = localStorage.getItem(STORAGE_KEY);
return text === null ? null : JSON.parse(text);
}That code belongs in a regular app page, not as a live persistence claim inside this sandboxed preview.
Checkpoint 5: polish the app contract
Once the core behavior works, make the app trustworthy.
Acceptance criteria:
- Empty states explain what the user can do next.
- Buttons that trigger in-flight work disable or communicate clearly.
- Task titles are written with
textContent, never parsed as HTML. - The state preview remains useful while debugging, then can be hidden as a stretch polish choice.
- The final board can be run, edited, fetched, filtered, saved to the detected adapter, and run again without uncaught errors.
Stretch goals
Choose one or two after the base board works:
- Add a "Clear completed" delegated action.
- Add task priority and render priority badges with
classList. - Add a hash route such as
#apior#donethat controls the active filter. - Add a retry button after a failed fetch.
- Add a tiny Canvas progress badge that redraws from task counts.
- Split storage, network, and rendering into small named modules once you reach Part V.
How to get unstuck
If the DOM drifts out of sync, return to State -> Render: UIs Without a Framework. If event handling gets noisy, revisit Bubbling, Capturing & Delegation. If network branches feel incomplete, compare your code to fetch() & REST APIs and Handling Failure Like a Pro. If storage behavior surprises you, reread localStorage, Cookies & IndexedDB. If URL state becomes a stretch goal, use Routing & the History API.
You have finished Part IV when this app makes the browser pieces feel connected: state drives the DOM, events change state, fetch brings outside data in, and storage is handled honestly even when the browser says no.