● David R. Longnecker
Chapter 6 · Frontend

Reactive UI Fundamentals

You could build every screen in this course by hand: create a <div>, set its textContent, listen for a click, mutate the DOM again. Frameworks exist because that approach falls apart the moment your data changes in more than one place at once. This chapter introduces the alternative: describing what the UI should look like for a given piece of data, and letting a framework keep the screen in sync for you.

The problem with manual DOM manipulation #

Say you're building a small todo list. Without a framework, every change to your data requires you to remember every place the screen depends on it:

updating the DOM by hand
const todos = [{ id: 1, title: "Write chapter 6", done: false }];

function addTodo(title) {
  todos.push({ id: Date.now(), title, done: false });
  renderList();      // don't forget this
  renderCount();      // ...or this
  renderEmptyState();  // ...or this
}

function renderList() {
  const ul = document.querySelector("#todo-list");
  ul.innerHTML = "";
  for (const todo of todos) {
    const li = document.createElement("li");
    li.textContent = todo.title;
    ul.appendChild(li);
  }
}

Every function that reads todos has to be called, in the right order, every time todos changes. Miss one call and the screen silently drifts out of sync with your data. This is not a hypothetical — it's the single most common source of UI bugs in hand-rolled DOM code. A component-based, reactive framework exists to remove this bookkeeping entirely.

Key idea

A component is a self-contained piece of UI: a template that describes structure, some data that drives it, and logic that updates the data. You stop thinking "which functions do I need to re-run" and start thinking "what does the screen look like given this data" — the framework figures out the rest.

Declarative templates #

The syntax below matches Vue 3's Composition API, which is what the rest of this course uses directly — see the official Vue introduction for the canonical reference. A template is HTML with a few extra pieces of syntax layered on top:

a todo list, declaratively
<template>
  <p v-if="todos.length === 0">Nothing to do yet.</p>
  <ul v-else>
    <li v-for="todo in todos" :key="todo.id">
      <span>{{ todo.title }}</span>
      <button @click="markDone(todo.id)">Done</button>
    </li>
  </ul>
  <p>{{ todos.length }} total</p>
</template>

Notice what's missing: there is no renderList(), no manual loop building <li> elements, no code that decides whether to show the empty-state paragraph. You wrote down what the UI is for a given todos array. The framework's job is to make that description true on screen, and to keep it true as todos changes.

Reactivity: ref and computed #

For the template above to update automatically, todos can't be a plain array — the framework needs to know when it changes. That's what reactive values are for. The two you'll use constantly are ref() and computed(), covered in depth in Vue's reactivity fundamentals guide. Both work the same way whether you call them from plain JavaScript or, as you'll see below, from inside a component's <script setup> block — the snippet below is shown on its own first so you can focus on what ref and computed actually do, before seeing where they live inside a real component.

ref and computed
import { ref, computed } from "vue";

const todos = ref([
  { id: 1, title: "Write chapter 6", done: false },
]);

const remaining = computed(() =>
  todos.value.filter((t) => !t.done).length
);

function markDone(id) {
  const todo = todos.value.find((t) => t.id === id);
  if (todo) todo.done = true;
}

ref() wraps a value so the framework can track reads and writes to it (you access or change the underlying value through .value in plain JavaScript — the template unwraps this automatically, which is why the earlier example writes todos instead of todos.value). computed() derives a new reactive value from other reactive values; it recalculates only when one of its dependencies changes, and anything displaying remaining updates the instant markDone() runs. You never call a "re-render" function yourself.

From data change to pixels: the virtual DOM #

When a reactive value changes, the framework doesn't tear down and rebuild the real DOM from scratch — that would be slow and would lose things like input focus and scroll position. Instead it builds a lightweight in-memory description of what the DOM should look like (the "virtual DOM"), compares it to the previous description, and applies only the differences to the real DOM. This comparison-and-patch step is what people mean when they say a framework "re-renders" a component.

A reactive value changes, the framework diffs a virtual DOM, and patches only what changed in the real DOM todo.done = true Reactivity tracks dependents Virtual DOM new vs. old tree Diff what actually changed? Real DOM patched, not rebuilt only the <button> text node updates
Changing one ref triggers a diff, not a full rebuild — only the affected DOM nodes are touched.

You rarely think about this step directly. The practical takeaway is: mutate the reactive value, and trust the framework to update the minimal set of real DOM nodes.

Single-file components #

A component's template, logic, and styling live together in one .vue file. The <script setup> block is where you declare refs, computed values, and functions; everything you declare there is automatically available to the template below it — no manual exporting or binding required.

TodoList.vue
<script setup>
import { ref, computed } from "vue";

const todos = ref([
  { id: 1, title: "Write chapter 6", done: false },
]);

const remaining = computed(() =>
  todos.value.filter((t) => !t.done).length
);

function markDone(id) {
  const todo = todos.value.find((t) => t.id === id);
  if (todo) todo.done = true;
}
</script>

<template>
  <ul>
    <li v-for="todo in todos" :key="todo.id">
      <span :class="{ done: todo.done }">{{ todo.title }}</span>
      <button @click="markDone(todo.id)">Done</button>
    </li>
  </ul>
  <p>{{ remaining }} remaining</p>
</template>

<style scoped>
.done {
  text-decoration: line-through;
  color: var(--fg-3);
}
</style>

The <style scoped> block's rules only apply to this component's own markup — you'll see exactly how that isolation works in Chapter 14. For now, the important shape to remember is three sections, one file: script for behavior, template for structure, style for appearance.

Tip

If a template feels like it's fighting you — too many v-if chains, logic leaking into markup — that's usually a sign the logic belongs in <script setup> as a computed value or a small function, not in the template itself. Templates describe; scripts decide.

Try it

In the TodoList.vue example above, add a second computed value, allDone, that's true when every todo has done: true. Then add a <p v-if="allDone"> congratulating the user. You won't need to touch markDone() at all — that's the point.