The last lesson of the previous module ended with an uncomfortable list: for Nómada Tasks to work the way it works, you have had to build by hand a declarative render, a reconciliation by stable key, a centralized state with invalidation, a systematic cleanup, a virtualized list and a code split. That list is, almost point for point, what a modern framework hands you already solved on day one. But "they give it to you solved" is not an explanation: it is a slogan. This lesson is the diagnosis before the remedy. You are going to see which concrete problems appear when you build an interface by hand —the eight you have already suffered and solved, named one by one—, what the jump from imperative to declarative really means and the UI = f(state) equation, how the three families of reactivity that share the landscape work on the inside, why the component is a better unit of reuse than your view/ modules, what kinds of state exist and why server state is a problem of a different nature, what the lifecycle is for, what separates a library from a framework, and —what almost no tutorial tells you— how much adopting one costs and when you should not. By the end you will have judgment, which is exactly what it takes to read the next four lessons without letting any of them dazzle you.
Contents
- The diagnosis before the remedy
- The eight problems of building an interface by hand
- The diagnosis table: problem → your solution → how a framework solves it
- Imperative versus declarative
UI = f(state): the equation and its consequences- The same piece of board, written both ways
- What "reactivity" means exactly
- Family 1: virtual DOM and reconciliation
- Family 2: fine-grained reactivity with signals
- Family 3: compilation at build time
- The three families in one table
- The component: template, state, behavior and styles
- Why the component is a better unit than your
view/modules - State: local, lifted, shared and server
- Why server state is a different problem
- The lifecycle and why it exists
- Library versus framework: what "opinionated" means
- The real cost of adopting a framework
- When NOT to use a framework
- The current landscape, to get your bearings
- What this module does and what it does not do
- Common Mistakes and Tips
- Exercises
- Conclusion
- The diagnosis before the remedy
There are two ways to learn a framework. The first is the usual one: open the documentation, copy the "hello world", learn the syntax, and find out six months later —sometimes never— what problem each piece was solving. The second is the one you, and only you, can afford, because you have taken the long road: look at the problem first, confirm that you have suffered it, and only then look at the solution each tool proposes.
The difference is not one of style. Whoever learns by the first route ends up using useMemo everywhere "because it optimizes", putting all the state in Redux "because that is what professionals do", and choosing a framework based on what they saw in a conference talk. Whoever learns by the second asks a different question: what concrete problem do I have, how much does it cost me now, and how much would it cost me with this?
Nómada Tasks is a real application today: six tasks on the board, 48 total hours, 45 open, 600 tasks in the load test, render() in 31 ms, 1,194 nodes, 58.3 kB across three requests and an LCP of 1.9 s. None of those numbers came free. Each one cost a lesson, and several cost two. That cost is the figure you need in order to judge any framework: you do not compare it against an ideal, you compare it against what you already have.
A word about method before starting. This module is not going to make you an expert in React, Vue or Angular. Four lessons are not enough for that, and anyone who tells you otherwise is selling you something. What it will give you is the mental model of each one: what problem it solves, with what mechanism, in exchange for what. With that model, learning any of them properly goes from being a month of bewilderment to a week of reading documentation and understanding what you read.
- The eight problems of building an interface by hand
Let us name them. They are not "JavaScript problems": they are problems of any interface that displays data that changes. They show up in Java Swing, in native Android, in iOS and on the web. Modern web frameworks did not invent them; they inherited forty years of attempts.
Problem 1 · Keeping state and screen in sync. The data lives in a variable; the screen shows it in a <span>. When the data changes, somebody has to remember to update the <span>. If three places display the open hours —the summary, the header and the tab title—, there are three places to update, and the bug consists of forgetting one. It is the most common error in interface programming and the hardest to catch in tests, because the screen does not "fail": it lies.
Problem 2 · The identity of the nodes when redrawing. The lazy way to solve problem 1 is to redraw everything. It works, but it destroys the nodes, and with them the focus, the text selection, the scroll position, the CSS transitions in flight and the browser's internal state (an open <details>, a video playing). You saw it in 06-06 with replaceChildren.
Problem 3 · The performance of the full redraw. With six tasks nobody notices. With 600 and an input that fires on every keystroke, redrawing everything is 310 ms of blocking per keypress. You measured it in 09-01 and left it at 31 ms in 09-04.
Problem 4 · Cleanup. Every addEventListener, every setInterval, every IntersectionObserver, every WebSocket subscription is one more thing that has to be switched off when its piece of screen disappears. If it is not switched off, you have a memory leak and, worse, a handler that keeps reacting to events from something that no longer exists. That was the whole of 09-03, with destroy() and AbortController.
Problem 5 · Composition. A task card appears on the board, in the details panel and in the report. If it is a loose function that receives an <li> and modifies it, reusing it in another context requires that context to know far too much about it. Reusing pieces of interface with their state and their behavior inside is not a problem that loose functions solve.
Problem 6 · Communication between distant parts. The assignee filter is changed by a <select> in the header, and it affects the list, the summary, the tab counter and the URL. You solved it with CustomEvent in 06-04, which works but turns the data flow into something you can only follow by searching for text strings across the whole project.
Problem 7 · The coupling between structure, style and behavior. The card's HTML is in a <template> in index.html, its styles are in css/styles.css and its behavior is split between card.js and controller.js. That is four files for one single thing. Renaming a CSS class means touching two of them and trusting your memory.
Problem 8 · Team conventions. You decided that views expose render, update and destroy; that custom events live in EVENTS; that actions are declared with data-action. Those are good decisions, but they are yours. Anybody new joining the project has to learn them by reading the code, because they are not written down anywhere except in habit.
- The diagnosis table: problem → your solution → how a framework solves it
This is the central table of the lesson. Read it slowly: the middle column is yours, and that is why the one on the right makes sense.
| # | Problem | Your solution in Nómada Tasks | How a framework solves it |
|---|---|---|---|
| 1 | Keeping state and screen in sync | The state → render() → event → new state → render() cycle from 06-06, with a single function that describes the whole screen |
Out of the box: you describe the screen as a function of the state and the framework takes care of running it again when the state changes |
| 2 | Node identity | reconcile(container, data, key, paint) with data-id (06-06) |
React's key, Vue's :key and Angular's track. It is literally your data-id, raised to an API requirement |
| 3 | Redraw performance | Map index, version cache (09-02), write batching and requestAnimationFrame (09-04), virtualization (09-04) |
Incremental reconciliation or fine-grained updates; virtualization is still yours (with libraries) |
| 4 | Cleanup | destroy() in every view + a shared AbortController (09-03) |
The lifecycle: useEffect's cleanup function, onUnmounted, ngOnDestroy. It calls itself |
| 5 | Composition | view/*.js modules that export functions and classes |
Components: template, state, behavior and styles in a single instantiable unit |
| 6 | Communication between distant parts | CustomEvent + EVENTS (06-04) |
props down, events up, and a shared store when that is not enough (you will see it in 10-03) |
| 7 | Structure/style/behavior coupling | Four files per card: <template>, CSS, card.js, controller.js |
One file per component, with scoped styles |
| 8 | Team conventions | Yours, unwritten | The framework's, documented, with tooling and with people who already know them |
Two readings of this table, both important.
The first: no row says "impossible". Everything a framework does can be done by hand, and you have done it. The difference is not one of capability, it is one of marginal cost: row 2 cost you twenty lines and half a lesson; in React it is a key property you write without thinking.
The second, less obvious: rows 7 and 8 are not technical. They are organizational. And in a team of five people they usually weigh more than the first six put together, because the real cost of a project is not in writing the code but in five people understanding it at the same time for three years.
- Imperative versus declarative
Here is the module's conceptual jump, and it is worth defining well because the two words are constantly misused.
Imperative programming: you describe the steps to reach the result. "Take the node with id 3, remove the class task--pending, add task--done, change the button's text to 'Reopen', disable the other button and update the column counter by subtracting one."
Declarative programming: you describe the result and let somebody else work out the steps. "A task in the done status looks like this." If the status is done, the screen shows that; how you get there from what was there before is not your business.
You already know the distinction without having named it. In 04-04 you compared this:
// Imperative: the steps
const open = [];
for (let i = 0; i < backlog.length; i++) {
if (backlog[i].status !== 'done') {
open.push(backlog[i]);
}
}with this:
The second is not "shorter": it is of a different nature. It mentions neither the index i, nor the destination array, nor the traversal order. It describes what open is (the tasks whose status is not 'done') and leaves the steps to the engine. If tomorrow JavaScript decided to parallelize filter, your code would still be correct; the loop with indices, not necessarily.
Applying that same distinction to the screen is what frameworks do. And there is a brutal asymmetry in the number of cases you have to consider.
- In imperative mode, to go from one state to another you have to write the transition. With n possible states, there are n×(n−1) transitions. With five states of a card (pending, in progress, done, overdue, unassigned) that is 20 transitions. Nobody writes them all; you write the ones you remember, and the bugs live in the ones you did not.
- In declarative mode, you write n descriptions: what each state looks like. The framework works out the transitions. Five descriptions instead of twenty transitions.
That is the whole argument. It is not elegance: it is that the number of things you have to write by hand goes from growing quadratically to growing linearly.
graph LR
subgraph Imperative
A1[State A] -->|transition 1| B1[State B]
B1 -->|transition 2| C1[State C]
A1 -->|transition 3| C1
C1 -->|transition 4| A1
B1 -->|transition 5| A1
C1 -->|transition 6| B1
end
subgraph Declarative
A2[State A] --> V[view describes the state]
B2[State B] --> V
C2[State C] --> V
end
UI = f(state): the equation and its consequences
UI = f(state): the equation and its consequencesThe condensed form of everything above is an equation you will see in the documentation of almost every modern framework:
The interface is the result of applying a pure function to the state. With the same state, the same screen, always. No hidden state in the DOM, no "it depends on what was there before".
The consequences run deeper than they look:
- The screen becomes reasonable. To find out why something looks the way it does, you look at the state. You do not have to mentally reconstruct the sequence of clicks that got you there. This is exactly what made imperative interfaces so hard to debug: the bug was not in the current state but in a transition that happened thirty seconds ago.
- The screen becomes testable. If
fis a function, you test it like a function: you give it a state and check the output. It is what you already did in 08-05 with Testing Library, rendering the view with a known board and querying by role. - The DOM stops being the source of truth. This is the biggest mental shift. In an imperative interface, "how many tasks are there?" is sometimes answered by counting
<li>s. In a declarative one, that is a conceptual error: the<li>is a consequence, not a piece of data. The source is the state. - The performance problem comes back. If
fproduces the whole screen, running it on every change means recreating everything. This is where reactivity comes in, which is the mechanism each framework uses to avoid paying that price. Sections 7 to 11 are about exactly that.
One thing deserves honesty: UI = f(state) is a model, not a literal description. There is state the DOM genuinely holds and the function does not control: the cursor position in an <input>, a container's scroll offset, which element has focus. Frameworks work very hard to preserve that state while pretending to redraw everything, and that effort is precisely reconciliation. When it fails —and it does fail— the strange bugs appear: an <input> that loses focus as you type, an animation that restarts. You will recognize the cause immediately, because it is your problem 2.
- The same piece of board, written both ways
None of this makes sense without code. Take the simplest operation in Nómada Tasks: marking task 6 as done, with its visible consequences (the card changes appearance, the button changes text, the pending counter goes down, the done counter goes up, and the open hours go from 45 to 40).
The imperative version, written by hand
// Imperative: we modify the screen step by step
function markDoneImperative(id) {
const task = board.findById(id);
task.changeStatus('done'); // 1 · the model
const li = document.querySelector(`[data-id="${id}"]`);
li.dataset.status = 'done'; // 2 · the attribute
li.classList.add('task--done'); // 3 · the class
li.classList.remove('task--overdue'); // 4 · it can no longer be overdue (R10)
const advance = li.querySelector('[data-action="advance"]');
advance.disabled = true; // 5 · there is no next status
advance.textContent = 'Completed'; // 6 · the button text
const reopen = li.querySelector('[data-action="reopen"]');
reopen.disabled = true; // 7 · from 'done' there is no reopening (R6)
const doneColumn = document.querySelector('[data-column="done"] .task-list');
doneColumn.append(li); // 8 · move it to the other column
updateCount('pending'); // 9 · counters
updateCount('done'); // 10
document.querySelector('#open-hours').textContent =
`${board.openHours()} h`; // 11 · the summary
document.title = `Nómada Tasks (${board.summary().pending})`; // 12 · the tab
}Twelve steps. All of them correct. And now the question that matters: what happens if tomorrow we add a priority badge to the card? Answer: you have to remember to update it here, and also in markInProgressImperative, and in reopenImperative, and in assignAssigneeImperative. Four places. The bug consists of touching three.
The declarative version, the one you already wrote
// Declarative: we describe the screen and then describe it again
function markDoneDeclarative(id) {
board.changeStatus(id, 'done'); // 1 · the model, and only the model
render(); // 2 · describe the whole screen again
}
function render() {
const visible = visibleTasks(state);
reconcile(list, visible, (t) => t.id, (t, node) => paintCard(t, node, TODAY));
$('#open-hours').textContent = `${state.board.openHours()} h`;
document.title = `Nómada Tasks (${state.board.summary().pending})`;
}Two steps. And the priority badge from the previous example is added in a single place: inside paintCard, which is the description of what a task looks like. Every action that triggers a render() will see it updated, without anybody having to remember.
Compare the two honestly:
| Aspect | Imperative | Declarative |
|---|---|---|
| Lines per action | ~12, and they grow with the interface | 2, constant |
| Places to touch when adding a visible piece of data | One per existing action | One, the description |
| Risk of an out-of-sync screen | High: it depends on memory | Zero by construction |
| Work for the browser | Minimal: only what changed | Potentially a lot: everything has to be compared |
| Ease of debugging | Low: you have to reconstruct the sequence | High: look at the state |
| Ease of testing | Low: you have to simulate the sequence | High: state → output |
Notice the one row where the imperative version wins: the work for the browser. That row is the bill for the declarative model, and it is exactly the one you paid with reconcile, with the version cache and with virtualization. Frameworks pay that same bill, with different mechanisms. Let us get to them.
graph TD E["State<br/>board + filters"] --> F["f(state)<br/>description of the screen"] F --> R["Reconciliation engine<br/>works out the minimum change"] R --> D["Real DOM"] D -->|user event| A["Action"] A -->|new state| E
- What "reactivity" means exactly
"Reactive" is the most worn-out word in the front-end vocabulary. Its technical meaning, however, is precise:
A system is reactive when a declared dependency between two values is maintained automatically: if the source changes, the destination updates without anybody asking.
The canonical example is not from programming, it is from a spreadsheet. You type the formula =A1+B1 into C1. You change A1 and C1 updates. You have not "called" anything: you declared a relationship and the system maintains it. That is the whole idea.
In an interface there are two relationships to maintain:
- State → derived state: if the tasks change, so do the open hours, the number of pending tasks and the filtered list.
- State → screen: if the open hours change, so does the
<span>that displays them.
You maintain the first with the version cache from 09-02 (recompute when the version counter does not match) and the second by calling render() by hand. It works, but it has two holes you know well: if you forget #invalidate() in a mutation, the cache lies; and if you forget to call render(), the screen lies. A reactive system removes both possible lapses. That is its entire value.
The interesting question is how the system finds out that something has changed. And that is where the world splits into three families.
- Family 1: virtual DOM and reconciliation
Who uses it: React (and, with nuances, Preact and Inferno).
The mechanism. Your f(state) function does not touch the DOM. It returns a description of the screen: a tree of lightweight JavaScript objects, with each element's type, its attributes and its children. That tree is the virtual DOM. It is the same idea as your paintCard, but instead of creating a real <li> it would create something like this:
// What a virtual description of a card would return
{
type: 'li',
props: { className: 'task task--high', 'data-id': 6 },
children: [
{ type: 'h3', props: { className: 'task__title' }, children: ['Carpentry workshop quote'] },
{ type: 'button', props: { 'data-action': 'advance' }, children: ['Start'] }
]
}When the state changes, the framework runs f again, gets a new tree, and compares the new one with the previous one (the diffing). Out of that comparison comes a list of minimal operations on the real DOM: "change this text", "remove this class", "delete this node". Only those operations are applied.
Why it needs keys. Comparing two lists of children is a hard problem in the general case. The optimal tree-difference algorithm has cubic cost, which is unworkable. Frameworks use linear-cost heuristics, and the most important one is: if two elements occupy the same position and have the same type, they are the same element. That heuristic fails exactly when the list is reordered or an element is removed from the middle — problem 2, the one you solved with data-id. The solution is identical to yours: ask the programmer for a stable key that identifies each element regardless of its position. In React it is called key.
The trade-offs, honestly:
- You pay in memory: there are two live virtual trees on every update.
- You pay in CPU: the whole new tree has to be built and walked while comparing, even if only one word changed. With 600 tasks, that is 600 descriptions created to discover that one of them changed.
- The cost does not depend on how much has changed, but on how much there is. That is its Achilles heel, and the reason
useMemo,React.memoand company show up: they are ways of telling the framework "do not go down this branch, it has not changed". - In exchange, the mental model is very simple: it is a function that runs again. There are no invisible subscriptions and no objects wrapped in proxies. When something goes wrong, what happened is that the function ran, or it did not run.
- Family 2: fine-grained reactivity with signals
Who uses it: Vue (with ref/reactive/computed), modern Angular (signal), Solid, Svelte in its runes version, Preact Signals and practically everything new.
The mechanism. Instead of comparing trees, the system records what depends on what while it runs. A reactive value (a signal) is not a value: it is a value with a list of subscribers. When it is read inside a tracking context, the signal notes that context down as a dependent of its own. When it is written, it notifies all of its dependents.
In pseudocode, and simplifying a great deal:
// A minimal signal implementation, to understand the mechanism
let currentObserver = null;
function signal(initialValue) {
let value = initialValue;
const subscribers = new Set();
return {
get() {
if (currentObserver) subscribers.add(currentObserver); // ← automatic registration
return value;
},
set(next) {
if (Object.is(value, next)) return; // no change, no work
value = next;
for (const s of [...subscribers]) s(); // ← notification
}
};
}
function effect(fn) {
const run = () => {
currentObserver = run; // while fn runs, reads are noted down
try { fn(); } finally { currentObserver = null; }
};
run();
}With that, the essentials already work:
const openHours = signal(45);
effect(() => {
document.querySelector('#open-hours').textContent = `${openHours.get()} h`;
});
openHours.set(40); // the <span> updates on its own: nobody called render()Read that last block again, because it contains the family's whole idea. Nobody called render(). The effect registered itself as a dependent when it read the signal, and the signal notified it when it changed. There is no tree comparison and no traversal: there is a list of subscribers and a call.
The decisive consequence: the cost of an update is proportional to how much has changed, not to how much is on screen. Changing the open hours updates one text node, whether the board has 6 tasks or 600. It is, conceptually, the same difference there was in 09-02 between walking an array and looking something up in a Map.
The trade-offs, with the same honesty:
- Tracking has a memory and setup cost: every reactive value drags its list of subscribers along.
- The mental model is subtler. Since registration happens on read, there are ways to "lose" reactivity without noticing: destructuring a reactive object copies the value and breaks the link; reading inside a
setTimeoutdoes not register because the context has already closed. Those are the classic Vue mistakes, and you will see them named and exemplified in 10-04. - When something does not update, the cause is invisible: there is no missing call, there is a dependency that did not get registered. Debugging that requires specific tools.
- In exchange, default performance is better and far fewer manual optimizations are needed. In practice,
useMemohas fewer necessary equivalents in the world of signals.
- Family 3: compilation at build time
Who uses it: Svelte was the first to make it its banner; Solid compiles JSX templates into direct DOM operations; Angular has always compiled its templates; Vue compiles its own and uses what it learns to skip pieces of the tree that cannot change.
The mechanism. The two previous families solve the problem in the browser, at runtime. This one solves it earlier: a compiler reads your component while the project is being built and generates JavaScript code that updates the DOM directly, with no generic engine to interpret it.
If you write a template that says "the number of open hours goes here", the compiler can see that this is the only point that depends on that variable and generate, literally, node7.textContent = openHours. There is no comparison and no generic subscription: there is an assignment, written by a machine that already knew where the slot was.
The trade-offs:
- Big advantage: the engine almost disappears from the bundle. What gets downloaded is your compiled code plus a handful of utilities, not a full reconciliation engine. It is why compiled frameworks have such small baseline bundles.
- Second advantage: the analysis work is done once on your machine, not a million times on users' phones.
- Disadvantage: the code you write is not JavaScript. It is a template language that looks like HTML and that only that compiler understands. The tooling (editor, ESLint, type checker, debugger) needs specific plugins, and the code you see in the debugger is not the code you wrote.
- Second disadvantage: magic is paid for in surprises. When the compiler cannot statically know what something depends on, it has to fall back on runtime mechanisms, and that is where odd rules you have to memorize come from.
- In practice the three families are blending: React has a compiler that inserts memoization automatically; Vue compiles and uses signals at the same time; Angular compiles and has adopted signals. The border is getting blurrier all the time.
- The three families in one table
| Criterion | Virtual DOM | Signals (fine-grained) | Compilation |
|---|---|---|---|
| When it decides what to update | At runtime, comparing trees | At runtime, following dependencies | At build time, analyzing the template |
| Unit that runs again | The whole component | The specific expression that depends on the data | The generated instruction |
| Cost of an update | Proportional to the size of the tree | Proportional to what has changed | Minimal: a direct assignment |
| Needs keys in lists | Yes (key) |
Yes (:key, track) |
Yes |
| Size of the downloaded engine | Larger | Medium | Very small |
| Mental model | Simple: it is a function that re-runs | Subtle: there are implicit dependencies | Opaque: the real code is written by the compiler |
| Usual manual optimization | Frequent (memoize, avoid renders) | Infrequent | Almost never |
| Typical bugs | Extra renders, wrong key |
Reactivity lost when destructuring | Unintuitive compiler rules |
| Third-party tooling | Maximum | Good | Requires specific support |
| Examples | React | Vue, Angular, Solid | Svelte, Solid, Angular |
A warning that saves pointless arguments: none of the three is "the right one". Each optimizes something different. The virtual DOM optimizes the simplicity of the mental model and freedom (everything is ordinary JavaScript). Signals optimize default performance. Compilation optimizes size and startup. In most real applications, with lists of dozens of items, all three are fast enough and the decision is made on other grounds: team, ecosystem, hiring.
- The component: template, state, behavior and styles
The module's second big concept, and the one that most resembles something you already know from other parts of engineering: the unit of reuse.
A component is a unit that groups together four things you have had separate until now:
| Part | What it is | In Nómada Tasks today |
|---|---|---|
| Template | The HTML structure it produces | <template id="task-template"> in index.html |
| State | The data only it cares about | Scattered: some in state, some in DOM attributes |
| Behavior | What it does when it receives events | controller.js, by delegation |
| Styles | Its appearance | A chunk of css/styles.css, with .task__* classes |
A component puts them together and under an explicit contract: it receives data through its props, emits events upward, and occupies a piece of screen for which it is entirely responsible.
Three properties make this unit work so well:
- It is instantiated. It is not "the card": it is a template from which six cards are created, each with its own internal state (for example, whether its action menu is open). With your
paintCard, that per-card state has no natural place to live and ends up in adatasetor in an externalMap. - It composes. A component contains other components, and the result is still a component.
<Board>contains three<Column>s, each of which contains n<TaskCard>s. It is exactly what HTML does, but with your own tags. - It isolates. Scoped styles stop the card's
.titleclass from affecting the header's.title. It is the visual equivalent of what the ES modules of 05-04 did with variable names: putting an end to the global namespace.
- Why the component is a better unit than your
view/ modules
view/ modulesLet us compare with what you have. js/view/card.js exports paintCard(task, existing, today). It is a good function: pure in intent, it serves both to create and to update, and it does not depend on the rest. But look at its seams:
// Your card's real contract, spread across four places
paintCard(task, existing, today) // js/view/card.js
<template id="task-template">…</template> // index.html ← coupling by global id
.task, .task--high, .task__title … // css/styles.css ← coupling by class name
[data-action="advance"] // js/view/controller.js ← coupling by attributeFour couplings, and nobody checks any of them. If you rename task-template, nothing fails until it runs. If you change .task__title in the CSS, the card looks wrong but there is no error. If you write data-action="advence", the button simply does nothing. These are failures that only an end-to-end test catches, and that is why they cost you three Cypress journeys in 08-06.
Now the three problems a component solves and your function does not:
Per-instance state. If each card needs to know whether its menu is open, where does that piece of data live? In your design, either you put it in the Task object (contaminating the model with view concerns), or you keep it in a view-level Map indexed by id (and remember to clean it up when the task disappears, or you get the leak from 09-03). In a component, it is a local variable of the component and it disappears with it.
Per-instance lifecycle. If the card of an overdue task needs a timer that updates "overdue by 15 d" at midnight, today you have to create the timer somewhere and remember to cancel it when the card is removed in reconcile. Your reconcile does not tell anybody that it has removed a node. In a component, there is an unmount point that calls itself.
Explicit contract. paintCard(task, existing, today) does not say which fields of task it uses. A component declares its props, and with TypeScript (which you will see appear in 10-05) that declaration is checked at compile time.
That said, let us be fair: your view/ module is the right thing for Nómada Tasks' size. Six tasks, one screen, one developer. The component starts paying off when there are twenty kinds of reusable element, each with its own state, and several people touching them at the same time.
- State: local, lifted, shared and server
The third big concept. Almost all the suffering in large applications comes from not distinguishing four things that go by the same name.
| Kind | What it is | Example in Nómada Tasks | Where it should live |
|---|---|---|---|
| Local | Only one piece of interface cares about it | Whether a card's action menu is open | Inside the component |
| Lifted | Two siblings share it, so it moves up to the common parent | The assignee filter, which affects both the <select> and the list |
In the nearest common ancestor |
| Shared (global) | Very distant parts of the tree need it | The signed-in user, the light/dark theme, the language | In a shared store |
| Server | It is a local copy of data that lives somewhere else | The tasks returned by listTasks() in 07-02 |
In a cache with rules of its own |
The practical rule, in an order that saves a lot of work: always start local, and move up only when you are forced to. The most expensive and most frequent error in modern front-end is the opposite: putting everything in a global store from day one "just in case". The result is a giant object where everything depends on everything, where nothing can be deleted out of fear, and where a unit test needs the whole application mounted.
You already know the first two kinds without having named them: your state object in app.js, with { board, filters, sort }, is exactly lifted state, pushed all the way to the top because the list, the summary and the router share it. And it works. The question in 10-03 will be: at what size does it stop working?
- Why server state is a different problem
This distinction is one of the most useful in the whole lesson, and the entire industry understood it late.
Local state is yours: you create it, you change it, you are the only source of truth. Server state is not yours. You have a copy, obtained at a specific moment, of something that lives on another machine and can change without telling you. That completely changes the questions you have to answer:
| Question | Local state | Server state |
|---|---|---|
| Who is the source of truth? | Your application | The server |
| Can it go stale? | No | Yes, at any moment |
| Does it have to be re-fetched? | Never | Yes: on window focus, on reconnect, every n seconds |
| Can reading it fail? | No | Yes: network, 500, timeout |
| Are there intermediate states? | No | Yes: loading, error, retrying, stale-but-showable |
| Is there concurrency? | Little | Yes: two requests coming back out of order |
| Is it shared between screens? | Sometimes | Almost always, and a single cache is advisable |
You have already lived through every one of those rows. In 07-03 you wrote fetchJson with ApiError, AbortController, timeouts and retries. In 07-04 you dealt with the BoardChannel's reconnection and with the batch of updates that arrives afterwards. In 07-03 you did optimistic UI: painting the change before the server confirms it, and rolling it back if it fails.
The conclusion, which you will pick up again in 10-03, is that storing server state in the same place as interface state is a category error. They are problems with different rules and they deserve different tools. That is why TanStack Query, RTK Query, SWR or Angular's resources exist: they are not "Redux but modern", they are remote-state caches with invalidation, retry and deduplication. You will see the concept in 10-02 and 10-03.
- The lifecycle and why it exists
A piece of interface goes through three moments, and there is work that can only be done at each one:
stateDiagram-v2 [*] --> Mounted: appears on screen Mounted --> Updated: state or props change Updated --> Updated: changes again Mounted --> Unmounted: disappears from the screen Updated --> Unmounted: disappears from the screen Unmounted --> [*]
| Moment | What you do there | Your equivalent today |
|---|---|---|
| Mount | Fetch data, subscribe to the WebSocket, start an IntersectionObserver, measure the real DOM |
BoardView's constructor and its first render() |
| Update | Repaint; react to a props change (for example, the id of the displayed task changes) | update() + reconcile() |
| Unmount | Switch everything off: remove listeners, cancel requests, clear timers, close channels | destroy() and the AbortController from 09-03 |
The lifecycle is not a framework curiosity: it is the answer to problem 4, the one that cost you a whole lesson. And it deserves an important nuance, because it is where most people get it wrong.
A framework guarantees that the cleanup function will be called, but does not guess what needs cleaning up. If you open a setInterval on mount and return nothing for cleanup, the interval stays alive exactly as it would in plain JavaScript. What the framework provides is the moment: a guaranteed place to put the shutdown, invoked automatically when the component disappears. That turns "remember to call destroy() from the right place" into "write the function's return". It is an enormous ergonomic improvement, not the removal of the problem.
- Library versus framework: what "opinionated" means
The classic distinction boils down to who calls whom:
- A library is code that you call. You drive the flow; the library solves a specific problem when you ask it to.
- A framework is code that calls you. It drives the flow; you fill in the slots it defines. This is what is known as inversion of control.
In practice the border is porous, but the consequence is clear enough: a framework decides things for you. And those already-made decisions are what people call "opinions".
| Decision | React (view library) | Angular (complete framework) |
|---|---|---|
| Router | You choose among several | Included and official |
| HTTP requests | You choose (fetch, axios, TanStack Query…) |
HttpClient included |
| Forms | You choose | Two systems included |
| Global state | You choose (10-03) | Services with signals, included |
| Language | JavaScript or TypeScript | TypeScript, effectively mandatory |
| Folder structure | Whatever you want | Whatever the CLI generates |
| Toolchain | You assemble it (or use a meta-framework) | The CLI does everything |
Neither column is better. They are different trades of the same scarce resource: decisions.
- Many opinions: a quick start with no arguing, every project in the company looks like the others, a new person finds their bearings in two days. In exchange, when you need to step off the marked path, it costs.
- Few opinions: maximum freedom, you pick the best tool for each piece. In exchange, every team assembles its own combination, and that combination has to be documented, maintained and updated. It is called "decision fatigue" and it is real: two teams in the same company both using React can have projects that look nothing alike.
A data point for calibrating your own case: Nómada Tasks is today a project with no opinions but yours. You chose Vite, Jest, Cypress, ESLint, the folder structure, the event names and the views' contract. It was a good and coherent decision. It also took you four modules.
- The real cost of adopting a framework
This is where almost all learning materials go quiet. A framework is not free. These are the five costs, with approximate figures so you get a sense of the order of magnitude (the concrete numbers age; the proportions rather less so).
Cost 1 · Downloaded weight. A reconciliation engine or a dependency injection system is code the user downloads and runs before seeing anything. Today's baseline bundles, compressed, range from a few kilobytes in compiled frameworks to several dozen in the most complete ones. Your entire application weighs 58.3 kB across three requests today. For a large application, adding the engine is irrelevant; for a widget on a marketing page, it can double the page's weight.
Cost 2 · Learning curve. It is not the syntax, which you learn in a day. It is the mental model: when your function runs again, why this effect fires twice, what a stable dependency is, why this does not update. Count on two weeks to two months before you are genuinely productive, and a good deal longer before you can comfortably debug a reactivity problem.
Cost 3 · Toolchain. A framework rarely comes alone. It brings a bundler, a compiler, editor plugins, specific ESLint rules, a testing environment of its own and, very often, TypeScript. That set has to be installed, configured, updated and fixed when it breaks. It is time not spent on the product and time that shows up in no demo.
Cost 4 · Lock-in. Code written for one framework cannot be moved to another without rewriting it. Your Board, your fetchJson, your date and format utilities are plain JavaScript: they are good anywhere, today and ten years from now. A component written for a specific framework is good as long as that framework exists and as long as its API does not change. This asymmetry has a very practical design consequence worth remembering: keep your business logic outside the framework. Let the view belong to the framework and the model belong to you. Nómada Tasks is already organized that way, and not by accident.
Cost 5 · Ecosystem churn. The libraries orbiting a popular framework change fast: the one recommended for routing five years ago may be abandoned today. Updating a project with twenty dependencies from that ecosystem is not a weekend: it is a project. This cost is proportional to popularity, which is a useful irony to keep in mind.
- When NOT to use a framework
An honest decision table, of the kind that does not usually appear on the front page of any documentation.
| Situation | Framework? | Why |
|---|---|---|
| Corporate site with little interactivity (a menu, a contact form) | No | The download and tooling cost does not pay off. HTML plus a bit of JavaScript, or a static site generator |
| A widget embedded in somebody else's website | No, or web components | A framework drags its engine along and can clash with whatever is already on the page. A native web component is a clean boundary |
| Internal application with forms, tables and permissions | Yes | This is exactly the case they were designed for: a lot of interface, a lot of state, a lot of reuse |
| Data dashboard with many views and navigation | Yes | Routing, code splitting and reusable components pay off from day one |
| One-person team, small and stable project | Probably not | The convention and hiring advantages do not apply; the cost does |
| Project that must keep working untouched for ten years | With great care | Standard JavaScript will keep working; an old version of a framework with unmaintained dependencies is a debt that accrues interest |
| Product with critical SEO and mostly static content | Only with server rendering, or better an islands approach | You will see why in 10-06 |
| Interface with extreme size requirements (kiosks, IoT, markets with very limited networks) | Compiled or nothing | Every kilobyte counts |
| Team that already masters one | Yes, that one | The team's productivity weighs more than any technical comparison |
And the most reliable signal of all, which you can apply to Nómada Tasks today: if you are writing by hand, for the second time, something a framework gives you ready-made, then the framework was already worth it. When you wrote reconcile you were on the borderline. When you wrote the virtualized list, you had already crossed it —unless the goal, as it was here, was precisely to learn how it works on the inside.
- The current landscape, to get your bearings
A short table to place yourself. It is not a comparison —that is lesson 10-06—, it is a map so the names stop sounding like noise.
| Tool | What it is | Reactivity | Distinguishing trait |
|---|---|---|---|
| React | View library | Virtual DOM (+ an emerging compiler) | Largest ecosystem, maximum freedom, most job demand |
| Vue | Progressive framework | Signals + compilation | Adopted little by little; coherent official ecosystem |
| Angular | Complete platform | Signals (previously Zone.js) | Everything included, TypeScript, dependency injection |
| Svelte | Compiler | Compilation + runes | The engine almost disappears; very little code written |
| Solid | Library | Fine-grained signals + JSX compilation | JSX with the performance of signals; no virtual DOM |
| Astro | Content meta-framework | None by default | Islands: static HTML with interactive pieces, from whichever framework you want |
| htmx | Small library | None | The server returns HTML; the client inserts it. It returns state to the server |
| Web components | Browser standard | None included | Native, no dependencies, they last as long as the platform lasts |
Two observations about this table. The first: the last three rows are not "minor alternatives", they are different takes on the problem. Astro and htmx question the premise that the interface should be built in the browser. If your application is mostly content with a little interaction, that premise is expensive and perhaps unnecessary. You will see it in 10-06.
The second: this module covers React, Vue and Angular because they are the three that concentrate most of the employment, the documentation and the existing projects, and because between the three of them they cover the three reactivity models and both extremes of the library-framework axis. Whoever understands the three understands the rest by reading their documentation.
- What this module does and what it does not do
An explicit warning, because it affects how you should read the next five lessons.
Module 11, the final project, is built in plain JavaScript. With Nómada Tasks exactly as it is: js/model/, js/data/, js/view/, its 124 Jest tests, its three Cypress journeys, its Vite and its service worker. There is no change of direction, there is no rewrite, and you are not going to need to install React to finish the course.
So what is this module for? For three concrete reasons:
- Judgment. You are going to work with frameworks, almost certainly. The difference between using them well and using them out of inertia is knowing what problem they solve. That is the main reason.
- Perspective on your own code. Seeing your
reconcileturned into akeyproperty, yourdestroy()into a cleanup function and your version cache into acomputedcasts light backwards over what you have built. You understand your own work better when you see it named by others. - An informed decision. By the end of 10-06 you will have seen the same screen written four ways, with its metrics. The final project being plain JavaScript will stop being what you know how to do and become what you have decided, which is not the same thing.
And there is one more practical reason: learning a framework properly requires a course of its own. What you can do in five lessons is understand the four approaches well enough to choose which one to learn in depth, and to read other people's code without feeling lost. That is the stated goal.
Common Mistakes and Tips
Believing that a framework makes the application faster. That is not true in general. It adds startup weight and a layer of work on every update. What it does is make it hard to write a slow interface by carelessness, because reconciliation avoids the full redraw most people would write by hand. You are no longer most people: your render() takes a measured 31 ms. Always compare against what you have, not against an assumption.
Choosing a framework based on performance comparisons. The differences between the big ones, in real applications, are milliseconds and get buried by decisions that do matter: how much data you request, how many images you load, how you deploy. Choosing by benchmarks of 10,000-row lists is optimizing a row you are never going to have.
Adopting one for a problem you do not have. If your page has a dropdown menu and a form, the framework is the problem, not the solution. The question is not "is it good?", but "what is it solving for me today?".
Putting everything in global state from day one. The most expensive and most frequent mistake. Start local, lift when two siblings need it, and use a shared store only when the tree forces you to. You will see it in detail in 10-03.
Mixing server state and interface state. Storing the response from listTasks() in the same place where you store "the modal is open" is putting two problems with different rules in the same box. The typical consequence: stale data that nobody knows when to refresh.
Putting business logic in the components. It is the failure with the highest medium-term price, because it is the one that makes the choice irreversible. Your rules R1–R10, your Board, your fetchJson: outside the framework. The component paints and collects events; the model decides. With that discipline, changing framework means rewriting the view; without it, it means rewriting the application.
Believing that the lifecycle cleans up for you. It gives you the place and the moment, not the content. An uncanceled setInterval is still a leak with a framework and without one. Everything you learned in 09-03 still applies.
Tip: learn one properly rather than three superficially. The mental model of reactivity transfers; the syntax, not so much. Whoever masters one reads the others with relative comfort. Whoever has done the tutorial of all three masters none.
Tip: prototype for a day with your hardest case. Not with the documentation's to-do list: with the worst thing you have. For Nómada Tasks it would be the 600-task list with real-time filtering and WebSocket updates. One day of prototyping teaches more than a month of comparisons, and it is the advice you will pick up again in 10-06.
Exercises
Exercise 1 · The diagnosis of your own code
Open (mentally, or for real if you have written it) js/view/board-view.js and js/view/controller.js. For each of the eight problems in section 2, answer:
- Have you solved it, partially solved it, or not solved it?
- How many lines of your code are dedicated to solving it?
- If tomorrow Nómada Tasks grew to five screens and thirty kinds of component, would that problem get more expensive, stay the same, or get cheaper?
Write the answer in a three-column table. The goal is not the accuracy of the numbers, but identifying which problems scale badly.
Exercise 2 · From imperative to declarative
This imperative code handles Nómada Tasks' assignee filter. Rewrite it in declarative style and explain what you have gained.
// Imperative
function filterByAssignee(name) {
const rows = document.querySelectorAll('.task');
let visible = 0;
let hours = 0;
for (const row of rows) {
const matches = name === null || row.dataset.assignee === name;
row.hidden = !matches;
if (matches) {
visible++;
hours += Number(row.dataset.hours);
}
}
document.querySelector('#count').textContent = `${visible} tasks`;
document.querySelector('#hours').textContent = `${hours} h`;
document.querySelector('#empty').hidden = visible > 0;
document.querySelector('#active-filter').textContent = name ?? 'All';
}Hints: look at where the data comes from (the DOM or the model?), at how many places get updated, and at what happens if a task changes assignee while the filter is active.
Exercise 3 · The honest decision
For each of these three projects, decide whether you would use a framework and which of the three approaches from section 20 fits best. Justify it with at least three criteria from this lesson and name explicitly the cost you are accepting.
- (a) Taller Nómada's public website: who we are, prices, photo gallery, contact form and an availability calendar that updates every hour. It has to rank well in search engines. One person maintains it three hours a month.
- (b) Nómada Tasks turned into a product for twenty workshops: five screens, role-based permissions, reports, real-time collaborative editing, a team of four people and a five-year horizon.
- (c) A "book your place" widget that Taller Nómada wants to offer to other websites so they can embed it with two lines of code. Those websites use WordPress, Shopify and worse things.
Solutions
Solution 1
Your table should look fairly close to this one. The line counts are approximate and what matters is the last column:
| # | Problem | State in your code | Approx. lines | Does it scale? |
|---|---|---|---|---|
| 1 | Keeping state and screen in sync | Solved: state → render cycle | ~40 (render + update) |
Yes, it scales well: the cycle does not grow with the application |
| 2 | Node identity | Solved: reconcile with data-id |
~20 | Badly: it only works for direct children with a key. With nested components it would have to be redone |
| 3 | Performance | Solved: index, cache, batching, virtualization | ~200 spread out | Badly: every new screen needs its own virtualization and its own cache |
| 4 | Cleanup | Solved: destroy() + AbortController |
~30 | Badly: it depends on somebody calling destroy(). With twenty components, forgetting is a matter of time |
| 5 | Composition | Partial: functions that paint, with no per-instance state | — | Badly: there is no natural place for a card's local state |
| 6 | Communication between distant parts | Partial: CustomEvent |
~25 | Badly: the flow can only be followed by searching for strings across the project |
| 7 | Structure/style/behavior | Unsolved: four files per card | — | Badly: four unchecked couplings, per component |
| 8 | Conventions | Partial: they exist, they are not written down | — | Badly: every new person learns them by reading code |
Conclusion of the exercise: problem 1 you have solved in a way that scales; the rest, you have not. That is, in one sentence, the entire argument in favor of frameworks for applications that grow — and the argument against them for applications that are not going to grow.
Solution 2
The first step is to diagnose the imperative code's three defects:
- The data comes out of the DOM (
row.dataset.hours,row.dataset.assignee). The DOM has become the database, and it is a bad database: everything is text, there is no validation, and anything that touches the HTML corrupts the calculations. - There are four update points (
#count,#hours,#empty,#active-filter) that have to be remembered. Adding a fifth visible piece of data forces you to touch this function and every other one that changes the filter. - It uses
hiddeninstead of not rendering. Hidden nodes still exist, take up memory, show up in the accessibility tree if it is done badly, and are found by Ctrl+F. With 600 tasks, there are 600 nodes for 6 visible ones.
The declarative version:
// Declarative: the filter is state; the screen is a consequence
function filterByAssignee(name) {
state.filters.assignee = name; // 1 · change the state
render(); // 2 · describe the screen again
}
function render() {
const visible = visibleTasks(state); // from the MODEL, not from the DOM
reconcile(list, visible, (t) => t.id, (t, node) => paintCard(t, node, TODAY));
const hours = visible.reduce((s, t) => s + t.estimatedHours, 0);
$('#count').textContent = `${visible.length} tasks`;
$('#hours').textContent = `${hours} h`;
$('#empty').hidden = visible.length > 0;
$('#active-filter').textContent = state.filters.assignee ?? 'All';
}What you have gained, concretely:
- A single source of truth. The numbers come from the
Taskobjects, with their correct types. If the HTML changes, the calculations are still correct. - A single place to update. Adding "weighted effort" to the panel is one line in
render(), and it works for every existing action, not just for the filter. - Correctness in the face of concurrent changes. If the
BoardChannelfrom 07-04 changes a task's assignee while the filter is active, the imperative version leaves that row visible with the wrong assignee until somebody filters again. The declarative one repositions it on the nextrender(), because there is no "memory" of what was already computed. - Fewer nodes.
reconcilegenuinely removes the cards that do not match, instead of hiding them: it is the difference between the 1,194 nodes and the 7,812 you measured in 09-04.
What you have lost, to be fair: the imperative filter only touches the rows' hidden property, while the declarative one walks the tasks, filters, sorts and reconciles. With six tasks it is indistinguishable; the declarative model always does more work per update, and in exchange it does that work well and written only once.
Solution 3
(a) Taller Nómada's public website: no client framework.
Criteria: interactivity is minimal (a menu, a form, a calendar that refreshes every hour); SEO is critical and the HTML has to arrive ready from the server; maintenance is three hours a month, which makes ecosystem churn (cost 5) the dominant risk — nobody is going to be around to update twenty dependencies.
Suitable approach: a static site generator or an islands approach in the style of Astro, with static HTML and a single interactive piece for the calendar. A little loose JavaScript for the menu and the form is enough.
Cost accepted: if in two years they want to add a private area with bookings and a user profile, it will have to be rethought. That is a reasonable cost compared with maintaining a toolchain for a contact form.
(b) Nómada Tasks as a product: a framework, without a doubt.
Criteria: five screens with navigation and code splitting; four people who need shared conventions and explicit contracts (problems 7 and 8); a lot of state shared between screens (permissions, user, filters); collaborative editing, which multiplies the server-state problems from section 15; and a five-year horizon, in which hiring and training weigh as much as the code.
Which one: any of the three, and the choice should be based on the team and the local market before the technology. If the team already knows one, that one. If they come from other platforms with dependency injection and types, Angular fits well; if freedom and a wide job market are valued, React; if a gentle curve and a coherent official ecosystem are valued, Vue. It is the decision in 10-06.
Cost accepted: two months of learning curve spread across four people, a toolchain to maintain, and real lock-in. It is mitigated with the discipline from section 18: Board, rules R1–R10 and fetchJson stay in plain JavaScript, outside the framework.
(c) The embeddable widget: native web components, or plain JavaScript.
Criteria: it runs on other people's pages whose CSS and JavaScript you do not control; weight matters a lot because it adds to a page that is not yours; style isolation is a requirement, not a luxury (the Shadow DOM genuinely provides it); and longevity matters, because you cannot ask a hundred websites to update your script.
Suitable approach: a native custom element with Shadow DOM, with no dependencies, or —if more interface is needed— a compiled framework that leaves a very small bundle and can be packaged as a custom element.
Cost accepted: writing more by hand, without a framework's conveniences, and solving section 2's eight problems yourself inside the widget. It is acceptable because the widget is small and its surface is bounded: precisely the case where a framework does not pay off.
Conclusion
You have made the diagnosis before looking at any remedy, which is the only way for the remedies to make sense. You know the eight problems that show up in any interface displaying data that changes —synchronization, node identity, redraw performance, cleanup, composition, communication between distant parts, coupling between structure, style and behavior, and team conventions— and you know exactly what your solution is for each one, how much it cost you and which of them scale badly as the project grows.
You understand the jump from imperative to declarative not as a question of elegance but of arithmetic: writing n state descriptions instead of n×(n−1) transitions. You have the UI = f(state) equation with its four consequences —the screen becomes reasonable, becomes testable, the DOM stops being the source of truth, and the performance bill appears that has to be paid with reconciliation. And you have seen the same marking of a task as done written both ways: twelve fragile steps versus two steps and a description.
You know precisely what reactivity means —a declared dependency that the system maintains on its own— and you know the three families with their real mechanisms: the virtual DOM that compares two trees and therefore needs a key that is literally your data-id, with a cost proportional to the size of the tree; signals, which record who reads what and notify on write, with a cost proportional to what changes and a subtler mental model where reactivity is lost on destructuring; and compilation, which solves the problem before reaching the browser, with minimal bundles in exchange for writing in a language only its compiler understands. None is the right one: each optimizes something different, and the three are blending.
You know what a component is —template, state, behavior and styles in one instantiable, composable, isolated unit— and why it is a better unit than your view/ modules, with their four unchecked couplings between the <template>, the CSS, card.js and controller.js, with no natural place for per-instance state and no warning when reconcile removes a node. You distinguish the four kinds of state —local, lifted, shared and server— with the rule of always starting local and moving up only when forced, and you understand why server state is a problem of a different nature: you are not its source of truth, it can go stale without telling you and it has intermediate states that local state does not have. And you know that the lifecycle exists to solve problem 4, giving you the guaranteed moment to switch off what you switched on, without guessing for you what needs switching off.
You are clear about the difference between a library and a framework —who calls whom— and about what "opinionated" means: decisions already made, that save arguments and cost freedom. And, above all, you know the real cost of adopting one: downloaded weight, two weeks to two months of learning curve, a toolchain to maintain, lock-in that is only mitigated by keeping business logic outside the framework, and ecosystem churn proportional to popularity. With its counterpart: the table of when NOT to use one, with the most reliable signal of all —if you are writing by hand for the second time something a framework gives you ready-made, it was already worth it.
It has been said explicitly: Module 11 is built in plain JavaScript, with Nómada Tasks exactly as it is, its 124 tests and its three Cypress journeys. This module is not a change of technology but of perspective. In the next four lessons you are going to see the same screen —the task list with its assignee filter and its mark-as-done button— reimplemented four times, so that the comparison is real and not a list of features. We start with the virtual DOM approach and with the library with the largest ecosystem: Introduction to React.
JavaScript Course: From Beginner to Advanced
Module 1: Introduction to JavaScript
- What Is JavaScript?
- Setting Up Your Development Environment
- Your First JavaScript Program
- JavaScript Syntax and Basic Concepts
- Variables and Data Types
- Basic Operators
- Type Conversion and Comparisons
- The Course Project: Nómada Tasks
Module 2: Control Structures
- Conditional Statements
- Loops: for, while, do-while
- Switch Statements
- Flow Control: break, continue and Nested Loops
- Error Handling with try-catch
Module 3: Functions
- Defining and Calling Functions
- Function Expressions and Arrow Functions
- Parameters and Return Values
- Scope and Closures
- Hoisting and the Execution Context
- Higher-Order Functions
- Recursion
Module 4: Objects and Arrays
- Introduction to Objects
- Object Methods and the
thisKeyword - Arrays: Basics and Methods
- Iterating over Arrays
- Searching, Sorting and Aggregating Data: find, sort and reduce
- Array Destructuring
- Object Destructuring, Spread and Rest
- JSON and Copying Objects
Module 5: Advanced Objects and Functions
- Prototypes and Inheritance
- Classes and Object-Oriented Programming
- Encapsulation: Getters, Setters and Private Fields
- Modules: Import and Export
- Asynchronous JavaScript: Callbacks
- Promises and Async/Await
- The Event Loop and the Microtask Queue
- Iterators and Generators
Module 6: The Document Object Model (DOM)
- Introduction to the DOM
- Selecting and Manipulating DOM Elements
- Handling Events
- Propagation, Delegation and Custom Events
- Creating and Removing DOM Elements
- Rendering Lists and HTML Templates
- Handling and Validating Forms
Module 7: Browser APIs and Advanced Topics
- Local and Session Storage
- The Fetch API and AJAX
- Robust Requests: Errors, Timeouts and AbortController
- WebSockets
- Service Workers and Progressive Web Apps (PWAs)
- Essential Browser APIs
- Introduction to WebAssembly
Module 8: Testing and Debugging
- Debugging JavaScript
- Code Quality: ESLint, Prettier and Conventions
- Unit Testing with Jest
- Test Doubles: Mocks, Stubs and Spies
- Integration Testing
- End-to-End Testing with Cypress
Module 9: Performance and Optimization
- Measure Before You Optimize: DevTools and Web Vitals
- Optimizing JavaScript Performance
- Memory Management
- Efficient DOM Manipulation
- Lazy Loading and Code Splitting
Module 10: JavaScript Frameworks and Libraries
- Why Frameworks Exist
- Introduction to React
- State Management with Redux
- Vue.js Basics
- Angular Basics
- Choosing the Right Framework
