Everyone Can Learn Program
中文 Start learning
Core · FrontendLesson 8 of 8Free to read

State and rendering · Frontend: HTML, CSS and JavaScript

How do apps keep the screen in sync with the data?

Course contents

Why 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).

  1. Keep all the data in one place: the state.
  2. Write render(state) that turns the state into HTML.
  3. 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 yourself

One 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));
Output
<ul><li>[x] Buy milk</li><li>[ ] Call mum</li></ul>
Step by step
  1. L2map turns each todo into one <li> string; the ? : picks a box.
  2. L3join("") glues the items together with nothing in between.

Update with actions

Try it yourself

Every 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);
Output
0 1
false true
Step by step
  1. L3A new array: the old items, plus one.
  2. L6A new array where only the toggled todo is a new object.
  3. L14s0 is still empty: update didn't change it.0 1
  4. L15s1 still has the old value; s2 has the new one.false true

Escape user text

Try it yourself

A comment that contains HTML must show as text, not run.

function escapeHtml(text) {
  return text
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;");
}

const comment = '<img src=x onerror="alert(1)">';
console.log(`<p>${comment}</p>`);
console.log(`<p>${escapeHtml(comment)}</p>`);
Output
<p><img src=x onerror="alert(1)"></p>
<p>&lt;img src=x onerror=&quot;alert(1)&quot;&gt;</p>
Step by step
  1. L3& first, so the & in &lt; isn't escaped again.
  2. L10Raw: a browser would load the broken image and run alert(1).
  3. 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>`);
  1. A
    <ul><li>a</li><li>b</li></ul>
  2. B
    <ul><li>a</li>,<li>b</li></ul>
  3. C
    <ul>[object Array]</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);
  1. A
    2
  2. B
    3
  3. C
    undefined
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.

  1. PredictReplay the actions

    Type exactly what this prints.

  2. 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 (escapeHtml is already written).

  3. 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 its qty; otherwise append { id, price, qty: 1 }
    • { type: "remove", id }: subtract 1 from its qty, and drop it when qty reaches 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

TermChineseDefinition
State状态The data a screen is drawn from.
Reducerreducer 更新函数A function (state, action) → new state.
Escaping转义Turning < > & " into entities so text can't become HTML.