Tutorial: 2. Lists and bindings

Tutorial

2. Lists and bindings

A count isn't a reading list. In this step the page keeps real books in an array, renders one row per book, and lets you tick them off.

A list of books

Replace src/routes/index.tsx again:

src/routes/index.tsx
interface Book {
	id: number;
	title: string;
	read: boolean;
}

let books = $state<Book[]>([
	{ id: 1, title: "The Left Hand of Darkness", read: true },
	{ id: 2, title: "Piranesi", read: false },
]);
let title = $state("");
let nextId = 3;
let unread = $derived(books.filter((b) => !b.read).length);

function add(e: SubmitEvent) {
	e.preventDefault();
	if (!title.trim()) return;
	books.push({ id: nextId++, title: title.trim(), read: false });
	title = "";
}

const Index = () => (
	<main>
		<h1>Reading list</h1>
		<form onSubmit={add}>
			<input placeholder="Book title" bind:value={title} />
			<button>Add</button>
		</form>
		<ul>
			{books.map((book) => (
				<li key={book.id}>
					<label>
						<input type="checkbox" bind:checked={book.read} />
						{book.title}
					</label>
				</li>
			))}
		</ul>
		<p>{unread} still to read.</p>
	</main>
);

export default Index;

Add a book with the form (Enter works too), and tick a box: the "still to read" count follows both.

Arrays and objects in state

$state wraps objects and arrays in a proxy that tracks each property separately. That's why books.push(…) updates the list: you don't need to build a new array. It's also why ticking a box works: book.read is a property of an object inside books, so changing it updates the row, and unread, which reads read on every book.

nextId is a plain let. Nothing on the page shows it, so it doesn't need to be reactive.

Keys

books.map(…) renders one <li> per book. The key tells Sigil which row belongs to which book. When the array changes, rows are matched by key: a new book gets a new row, a removed book loses its row, and a reordered list moves the existing rows instead of rebuilding them.

Use a stable id from your data as the key, not the array index.

Bindings

bind:value={title} replaces the value + onInput pair from step 1. It writes the input into title as you type and writes title back into the input when it changes, which is how title = "" clears the box.

bind:checked={book.read} does the same for a checkbox, and it works on a property: ticking the box sets book.read on that book.

The JSX reference lists every binding, including bind:group for radio buttons and bind:this for getting hold of an element.

Next: components and styles.

Edit this page