Students predict that three versions of the same app (Vue, Preact, Svelte) behave alike, sort each piece of code by the idea it is, find React's update catch in Preact, add an if in Svelte and a Clear button in Preact, and add one feature to all three.
Load these into Waypoints Prepare: choose Load URL and paste the copied address, or download the file and import it. In Prepare, the worksheet's Teacher edition shows the answers; the student copy hides them.
Predict and Run 10 min · Investigate 15 min · Modify 10 min · Make 15 min
An optional lesson for students who finished the to-do app. The point: components, state, derived (worked-out) values and events are the same ideas in every framework; only the syntax and the update rules differ.
useState). Its page is written with htm, a way to write HTML-like tags in a JavaScript string, because real React uses JSX, which needs a build step. Its update rule: the component function runs again after every change, so a plain const count = stars.length is always right, but you must replace data, never change it in place. stars.push(...); setStars(stars) gives Preact the same list, so it skips the update: the planted bug, and the most common React mistake.$state and $derived are its "runes". Its data can be changed in place (stars.push). Here the compiler runs in the browser (about 1 MB, loaded only by this lesson), reading the component from a hidden <textarea data-svelte>; in a real project it is a file called App.svelte, compiled by a build tool.Every library is served from this site, not a CDN.
The three spellings blur together. Keep returning to the four ideas: ask "which line is the data? the worked-out value? the event?" in each panel.
| What you will see | A question to ask | The fix |
|---|---|---|
| Preact's page does not change. | Does setStars get a new list, or the old one changed? | setStars([...stars, "⭐"]) or stars.slice(0, -1): always a new list. |
| Svelte shows a red error about reading the component. | Which line of the component does the message name? | Check the braces { } and the {#if} … {/if} pair on that line. |
| The Svelte app does not appear at all. | Is svelte.js loaded after the textarea, and does the textarea have data-svelte? | Keep the textarea with data-svelte hidden, and the script src="svelte.js" line after it. |
One possible solution for each task. Other correct programs are fine.
<h2>Svelte</h2>
<textarea data-svelte hidden>
<script>
let stars = $state([]);
let count = $derived(stars.length);
function add() {
stars.push("⭐");
}
</script>
<button onclick={add}>Add a star</button>
<p>Stars: {count} {stars.join("")}</p>
{#if count >= 5}
<p>That's a lot of stars!</p>
{/if}
</textarea>
<script src="svelte.js"></script>
<h2>Preact</h2>
<div id="preact-app"></div>
<script src="preact.js"></script>
<script>
const { html, render, useState } = htmPreact;
function App() {
const [stars, setStars] = useState([]);
const count = stars.length;
function add() {
setStars([...stars, "⭐"]);
}
function clear() {
setStars([]);
}
return html`
<button onClick=${add}>Add a star</button>
<button onClick=${clear}>Clear</button>
<p>Stars: ${count} ${stars.join("")}</p>`;
}
render(html`<${App} />`, document.querySelector("#preact-app"));
</script>
<h2>Vue</h2>
<div id="vue-app">
<button @click="add">Add a star</button>
<button @click="remove">Remove one</button>
<p>Stars: {{ count }} {{ stars.join("") }}</p>
</div>
<script src="vue.js"></script>
<script>
const { createApp, ref, computed } = Vue;
createApp({
setup() {
const stars = ref([]);
const count = computed(() => stars.value.length);
function add() {
stars.value.push("⭐");
}
function remove() {
stars.value.pop();
}
return { stars, count, add, remove };
}
}).mount("#vue-app");
</script>
<h2>Preact</h2>
<div id="preact-app"></div>
<script src="preact.js"></script>
<script>
const { html, render, useState } = htmPreact;
function App() {
const [stars, setStars] = useState([]);
const count = stars.length;
function add() {
setStars([...stars, "⭐"]);
}
function remove() {
setStars(stars.slice(0, -1));
}
return html`
<button onClick=${add}>Add a star</button>
<button onClick=${remove}>Remove one</button>
<p>Stars: ${count} ${stars.join("")}</p>`;
}
render(html`<${App} />`, document.querySelector("#preact-app"));
</script>
<h2>Svelte</h2>
<textarea data-svelte hidden>
<script>
let stars = $state([]);
let count = $derived(stars.length);
function add() {
stars.push("⭐");
}
function remove() {
stars.pop();
}
</script>
<button onclick={add}>Add a star</button>
<button onclick={remove}>Remove one</button>
<p>Stars: {count} {stars.join("")}</p>
</textarea>
<script src="svelte.js"></script>
Stretch: Make each framework's star list into a component of its own. Find the same app in React on react.dev and compare it with the Preact version.
Support: Do the sort together on the board; students do only the Svelte if.
Name one idea that all three frameworks share, and one thing that differs.
Shared: data that changes and a page that follows it (or worked-out values, events, components). Different: the spelling, or the update rule (Preact replaces data, Vue and Svelte can change it).
Real projects: npm create vue@latest (Vue), Vite with React, or npx sv create (Svelte), each with one file per component. Class discussion: why might a team choose each one (popularity and jobs, simplicity, speed)?