State and rendering · Frontend: HTML, CSS and JavaScript
How do apps keep the screen in sync with the data?
Course contentsWhy you need it
A to-do list, a shopping cart, a chat: the data changes all the time, and the screen must always match it. Doing that by hand, element by element, gets messy fast. React, Vue and Svelte all share one simple idea that keeps it under control, and you can learn it with plain functions.
The intuition
The screen is a function of the data: screen = render(state).
- Keep all the data in one place: the state.
- Write
render(state)that turns the state into HTML. - When something happens (a click, a key press), compute a new state with
update(state, action), then render again.
update never changes the old state. It returns a new one built with ... spread and map/filter. That makes bugs easy to find: each action takes you from one known state to the next.
In a real page, wiring it up takes a few lines:
let state = [];
button.addEventListener("click", () => {
state = update(state, { type: "add", title: input.value });
list.innerHTML = render(state);
});Frameworks do the last line for you, and only touch the parts of the page that changed.
One rule for render: text that came from a user must be escaped before it goes into HTML. Otherwise someone can type a <script>-like tag into a comment and run code in other people's browsers: that's XSS.
Definition
State
All the data the screen depends on, kept in one place.
Render
Turning state into what's on screen: here, a function from state to an HTML string.
Immutable update
Building a new object or array instead of changing the old one, with spread, map and filter.
Examples
Render a list
Try it yourselfOne function turns the data into HTML.
function render(todos) {
const items = todos.map(t => `<li>${t.done ? "[x]" : "[ ]"} ${t.title}</li>`);
return `<ul>${items.join("")}</ul>`;
}
const todos = [
{ title: "Buy milk", done: true },
{ title: "Call mum", done: false },
];
console.log(render(todos));<ul><li>[x] Buy milk</li><li>[ ] Call mum</li></ul>
- L2map turns each todo into one <li> string; the ? : picks a box.
- L3join("") glues the items together with nothing in between.
Update with actions
Try it yourselfEvery change is an action; update returns the next state.
function update(todos, action) {
if (action.type === "add") {
return [...todos, { title: action.title, done: false }];
}
if (action.type === "toggle") {
return todos.map((t, i) => (i === action.index ? { ...t, done: !t.done } : t));
}
return todos;
}
const s0 = [];
const s1 = update(s0, { type: "add", title: "Buy milk" });
const s2 = update(s1, { type: "toggle", index: 0 });
console.log(s0.length, s1.length);
console.log(s1[0].done, s2[0].done);0 1 false true
- L3A new array: the old items, plus one.
- L6A new array where only the toggled todo is a new object.
- L14s0 is still empty: update didn't change it.
0 1 - L15s1 still has the old value; s2 has the new one.
false true
Escape user text
Try it yourselfA comment that contains HTML must show as text, not run.
function escapeHtml(text) {
return text
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
const comment = '<img src=x onerror="alert(1)">';
console.log(`<p>${comment}</p>`);
console.log(`<p>${escapeHtml(comment)}</p>`);<p><img src=x onerror="alert(1)"></p> <p><img src=x onerror="alert(1)"></p>
- L3& first, so the & in < isn't escaped again.
- L10Raw: a browser would load the broken image and run alert(1).
- L11Escaped: the browser shows the characters as plain text.
Common mistakes
Changing the state in place
state.push(item) changes the old state too, so you can't compare before and after, and frameworks may not notice the change. Return a new array: [...state, item].
Forgetting join
Putting an array straight into a template string joins it with commas: <li>a</li>,<li>b</li>. Use .join("").
Inserting raw user text
Anything a user typed must be escaped before it goes into HTML (or set with textContent instead of innerHTML). This is how XSS attacks are stopped.
Quick quiz
1. What does this print?
const items = ["a", "b"].map(x => `<li>${x}</li>`);
console.log(`<ul>${items}</ul>`);Show the answer
Answer: B
An array inside ${ } becomes its items joined with commas.
2. What does this print?
const before = [1, 2];
const after = before;
after.push(3);
console.log(before.length);Show the answer
Answer: B
after = before doesn't copy: both names point to the same array, so push changes "before" too.
Practice
3 exercises in three steps: predict what a program prints, change a program to meet a new requirement, then write one from scratch. Your code runs in the browser and is checked the moment you submit.
- PredictReplay the actions
Type exactly what this prints.
- ModifyFix the render
render(names)should return a list like<ul><li>Ann</li><li>Bo</li></ul>. It has two bugs: commas appear between the items, and a name containing HTML isn't escaped. Fix both (escapeHtmlis already written). - WriteA shopping cart
Write
update(cart, action)for a cart: an array of{ id, price, qty }.{ type: "add", id, price }: if the item is in the cart, add 1 to itsqty; otherwise append{ id, price, qty: 1 }{ type: "remove", id }: subtract 1 from itsqty, and drop it whenqtyreaches 0- any other action: return the cart unchanged
Never change the cart you were given: return a new array.
Practise this lesson
Open the interactive lesson to edit and run these examples and solve the exercises with instant feedback. Free to start, no sign-up.
Interactive lessons open in order. If you have not done the earlier ones yet, you start at the course map.
Terms in this lesson
| Term | Chinese | Definition |
|---|---|---|
| State | 状态 | The data a screen is drawn from. |
| Reducer | reducer 更新函数 | A function (state, action) → new state. |
| Escaping | 转义 | Turning < > & " into entities so text can't become HTML. |