Topic 27.1
Plan the App
In one line
Before writing code, plan the task board with the RADIO steps from Phase 25: list the requirements, sketch a mockup, turn it into a component tree, decide what is state and where it lives, and design the data model and API.
Think of it like this
Building a treehouse. You don't start by nailing random planks. You decide what it must have (a ladder, a roof, room for three friends), draw it, list the parts, and only then cut wood. Ten minutes with a pencil saves a weekend of pulling nails out. Planning an app is the pencil drawing.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Capstone
- A final project that uses everything you learned, like the top stone that finishes an arch.
- Task board (Kanban)
- A board of cards in columns such as To do, Doing and Done. Moving a card shows its progress.
- RADIO
- A planning checklist from Topic 25.1: Requirements, Architecture, Data model, Interface, Optimisations.
- Functional requirement
- Something the app must do, like "move a task to Done".
- Non-functional requirement
- How well the app must work: speed, accessibility, surviving reloads and errors.
- Mockup
- A rough drawing of the screen, made before any code.
- Derived value
- Data you can compute from other data, like a column's count. Compute it, don't store it.
- Data model
- The shape of your data: which fields each thing has and how things relate.
- Action
- A plain object describing what happened, like
{ type: "moved", id: "t3", status: "done" }.
Step by step
01R: write the requirements
Functional: show three columns; add a task; edit its title; move it left or right; delete it; search by title; filter by priority; open a read-only archive of finished tasks.
Non-functional: keyboard only works everywhere; moves are announced to screen readers; tasks survive a reload; the UI answers instantly even though the server takes 300 ms; a failed save is undone and explained; the archive of 5,000 tasks scrolls smoothly; the code has tests.
02A: mockup to component tree
Draw the screen, box each part, name the boxes. The tree below is what you'll build in 27.2. Notice Column doesn't know about tasks: it takes children, so the same column could hold anything (composition, Topic 2.5).
03D: the data model
One flat array of tasks. Columns are a constant list, not state: they never change while the app runs.
export const COLUMNS = [
{ id: "todo", title: "To do" },
{ id: "doing", title: "Doing" },
{ id: "done", title: "Done" },
];
// A task: { id: string, title: string, status: "todo" | "doing" | "done", priority: "high" | "medium" | "low" }
export const SEED_TASKS = [
{ id: "t1", title: "Plan the board", status: "done", priority: "high" },
{ id: "t2", title: "Build static components", status: "doing", priority: "high" },
];04Decide where each piece of state lives
Put state as low as possible but as high as needed (Topic 6.1). Shared and saved data goes up into a store; shareable view settings go into the URL; private UI details stay in the component that owns them.
05I: the API and the actions
Write the contract before the code. The reducer handles added, edited, moved, deleted and loaded. The fake server offers listTasks() and moveTask(id, status), both slow and both able to fail. With the contract fixed, the UI and the API can be built and tested separately.
GET /tasks -> 200 [Task] (about 300 ms; may fail with 503)
PATCH /tasks/:id { status } -> 204 (about 300 ms; may fail with 503)
Reducer actions: added { id, title } | edited { id, title } | moved { id, status }
deleted { id } | loaded { tasks }06O: plan the optimisations and the folder layout
Plan the few optimisations the requirements demand (optimistic moves, a virtualised archive, a lazy archive route) and nothing else. Measure before adding memo (27.5). Then fix a folder layout so every later milestone knows where its code goes.
task-board/
├── index.html
├── package.json
├── vite.config.js
└── src/
├── main.jsx createRoot, router, error callbacks
├── App.jsx layout, routes
├── data/seed.js COLUMNS, SEED_TASKS
├── api/fakeApi.js listTasks, moveTask (later: real fetch)
├── state/
│ ├── tasksReducer.js pure reducer (+ tasksReducer.test.js)
│ ├── taskStore.js external store over localStorage
│ ├── TasksProvider.jsx contexts + useTasks/useDispatch hooks
│ └── actions.js moveTask: optimistic + rollback
├── hooks/useLoadTasks.js loading / error / retry
├── components/ Board, Column, TaskCard, AddTask, Filters, VirtualList
└── routes/Archive.jsx lazy-loaded archive pageTry it yourself
- 1
Change the filter, predict the counts
In the first example, set
filtersto{ q: "", priority: "high" }. Predict the visible ids and the count per column before running. ([ 't1', 't2' ], and one each in Doing and Done.) - 2
Add a requirement
Suppose tasks need a due date and the board must show overdue tasks in red. Which part of the plan changes? (The data model gets
due; "is overdue" is derived, not stored; nothing else moves.) Adddue: "2024-01-20"to one task in the first example and log the overdue ones against the fixed datenew Date("2024-01-15T10:30:00Z"). - 3
Extend the wireframe
Add a
Box name="Archive"with aBox name="VirtualList"inside to the wireframe. The console now lists two more components.
Code & diagrams
Four tasks are stored; everything else is computed from them and the filters. Change `filters.q` to `""` and every task is visible again: derived values can't go stale.
Preview
Console
Nothing logged yet.Expected console output
stored tasks: 4
visible after the filter: [ 't3' ]
count per column: { todo: 1, doing: 0, done: 0 }A mockup in code: every dashed box is a component, labelled from its `data-name`. Repeated boxes (Column, TaskCard) become one component used many times.
Preview
Console
Nothing logged yet.Expected console output
components: App, Header, Filters, AddTask, Board, Column, TaskCard, Announcer (sr-only)
TaskCard boxes: 6Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Store a value you could derive
In the first example, store the counts as data: const counts = { todo: 2, doing: 1, done: 1 };, then add a fifth task with status: "todo" to tasks without touching counts.
Myth vs fact
Myth
Planning is for big teams; a solo project should start coding.
Fact
A 15-minute plan (requirements, tree, state table) prevents the expensive mistakes: state in the wrong place, a data model that needs a rewrite, accessibility bolted on at the end.
Myth
Each column should have its own array of tasks.
Fact
Three arrays mean a move is two edits that must stay in step, and a task can end up in two columns. One flat array with a status field makes moving a one-field change.
Myth
Everything shared should go into one global store.
Fact
Put each piece where it belongs: shareable view settings in the URL, private UI details in components, and only shared, changing data in a store.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The state table is also your test plan: every row with "state" needs a test that changes it, and every derived row needs a test that it follows the state (27.5).
- ▸
Name actions as events (
moved) rather than setters (setStatus). Event names keep the reducer the single place that decides what an event means, which is what makes undo, logging and replay possible later. - ▸
Non-functional requirements decide the architecture more than features do: "survives reload" asks for a persistent store, "shareable filters" asks for the URL, "5,000 rows" asks for virtualisation. Write them first.
- ▸
In an interview (Phase 26), the same RADIO plan is what you say out loud in the first ten minutes of a frontend system design or machine-coding round.
Remember this
- 1
What we're building. A task board (a Kanban board, after the Japanese word for a signboard): tasks are cards in three columns, To do, Doing and Done. You can add, edit, move and delete cards, search and filter them, and they are saved. Behind it sits a pretend server (a program on another computer that stores the real data) that is slow and sometimes fails. An archive holds 5,000 finished tasks. Every step uses something you learned earlier, and this topic plans it with the RADIO framework from Topic 25.1: Requirements, Architecture, Data model, Interface (the API), Optimisations.
- 2
Requirements come in two kinds. Functional requirements say what the app does: add a task, move it, filter by priority. Non-functional requirements say how well it does it: it works with a keyboard only, it announces changes to screen readers, it stays fast with 5,000 archived tasks, it survives a reload, it handles a slow or failing server. Write both lists before coding and keep them short; they become your test list in 27.5.
- 3
Mockup → component tree is Thinking in React (Topic 2.7). Draw the screen, then draw a box around each part with one job:
App,Header,Filters,AddTask,Board,Column,TaskCard, and an invisibleAnnouncerfor screen readers. Boxes inside boxes become the component tree. Repeated boxes (three columns, many cards) are one component used many times. - 4
The state plan decides what data changes and where it lives (Topic 2.7, Phase 6). Ask of each piece: does it change? Can it be derived (computed from other data)? Who needs it? The tasks change, many components need them and they must be saved, so they live in a reducer behind context (Topic 9.6) and an external store (Topic 10.3). The search text and priority filter should survive a reload and be shareable as a link, so they live in the URL (Topic 13.3). The text of a new task lives in
AddTask; "which card is being edited" lives in thatTaskCard. The visible tasks and the column counts are derived: computed while rendering, never stored, so they can never go out of date. - 5
The data model is the smallest set of data that describes everything. A task is
{ id, title, status, priority }, wherestatusis"todo","doing"or"done". Tasks are kept in one flat array, not three arrays (one per column): moving a card changes one field, and a task can never be in two columns at once. Theidis a stable string that never changes, used as the Reactkey(Topic 3.3) and in the API. - 6
The interface (API) lists the server calls and the actions in the reducer. Server:
listTasks()andmoveTask(id, status), each taking about 300 ms and able to fail. Reducer actions:added,edited,moved,deletedandloaded. Naming actions as events that happened (past tense) keeps the reducer readable. Optimisations we plan up front, not later: optimistic moves (27.4), a virtualised archive (24.1, 27.4),memoonly where render counts show waste (27.5) and a lazy-loaded archive route (27.6).
Explain it without notes
Walk through how you'd plan a task board before coding.
Why keep tasks in one flat array instead of one array per column?
Where should the search text live, and why?
What is a derived value? Give two in the task board.
Practice
Write the state table rows for a new feature: a "Show only my tasks" toggle and a task assignee field.
Write the action objects for: adding "Fix bug", moving t3 to Done, and deleting t1.
Trade-offs
- ↔
A detailed plan up front saves rework but can freeze ideas too early: plan the structure and the state, and leave visual details for later.
- ↔
Putting filters in the URL makes them shareable but means every filter change goes through the router; for state nobody would share (an open menu), component state is simpler.
Done when you can
I can write functional and non-functional requirements for an app.
I can turn a mockup into a component tree.
I can decide for each piece of data whether it is state, derived or URL state, and where it lives.
I can design a flat data model with stable ids.
I can list reducer actions and API calls before writing code.