Components & Composition
Learning Objectives
- You can define and compose Svelte components with explicit props.
- You can distinguish parent-owned data from child presentation.
- You can test a component through observable content rather than implementation details.
In Part 1, the practice page composed Counter and TodoList; Part 1’s marketplace page added a listing view. We inspected those boundaries; now we will design components that receive data from a parent. A component gives a portion of the interface an explicit boundary: inputs arrive, markup is produced, and interactions can be communicated to its parent.
We will use a small book catalogue for the worked example. The practice task uses conference sessions, and the marketplace transfer comes later.
Set up the component lab
In the practice application (practice/), create client/src/lib/examples/ for neutral components and client/src/routes/lab/+page.svelte as a scratch page for mounting them. Visit /lab while studying examples; preserve the counter and todo page at /. These files stay in the practice application. The marketplace project adapts their patterns in the separate marketplace repository.
Each example names its files. You may replace the scratch page as you move between examples, but do not paste several complete <script> blocks into one component. Files ending in .test.ts belong beside the code under client/src, which the existing client service mounts.
A component has inputs
Create client/src/lib/examples/BookCard.svelte:
<script lang="ts"> type Book = { title: string; author: string; available: boolean }; let { book }: { book: Book } = $props();</script>
<article> <h2>{book.title}</h2> <p>By {book.author}</p> <p>{book.available ? 'Available' : 'On loan'}</p></article>$props() declares the component’s inputs. book is a value supplied by a parent, not data fetched automatically by Svelte. The type describes the shape expected by tooling. It does not validate arbitrary JSON at runtime.
The braces in markup evaluate expressions. A title is inserted as text, not interpreted as HTML. Keep that default behavior for ordinary user content.
The Svelte documentation describes component props. In this course we use the runes-based syntax.
A page composes components
Put this in client/src/routes/lab/+page.svelte:
<script lang="ts"> import BookCard from '#lib/examples/BookCard.svelte';</script>
<main> <h1>Library catalogue</h1> <BookCard book={{ title: 'A Small Atlas', author: 'M. Reed', available: true }} /> <BookCard book={{ title: 'Night Gardens', author: 'L. Moss', available: false }} /></main>The two instances share an implementation, not a particular book. Each receives different data. The supplied #lib/* mapping resolves to src/lib/*; keep the pinned project’s import convention.
A useful component boundary groups a coherent responsibility. A book card can be tested independently of catalogue fetching. Turning every paragraph into its own component would add indirection without necessarily clarifying anything.
Props are not a second source of truth
A child should normally treat parent-owned data as input. Do not copy a prop into separate state merely to display it, and do not mutate a parent’s object as an informal communication channel.
For example, the card should not quietly change book.available when clicked. A later parent might need to check permission, ask the API, or reconcile an error. The parent should own that decision. Chapter 2.5 introduces callback props for communicating intent.
Composition can also use snippets for replaceable content. We need one such case for layouts later, but reusable components do not require an elaborate snippet API from the beginning. Plain data props are enough for this card.
A replaced workshop prop
0 / 5 points
A parent passes a workshop record to a card. The parent can later replace it
with a different record. The card currently copies workshop.title into an
ordinary variable once and renders that copy. Which change addresses stale titles?
Test the component’s visible contract
The practice application already includes Vitest, a DOM test environment, and Svelte Testing Library. Create client/src/lib/examples/BookCard.test.ts:
import { cleanup, render, screen } from '@testing-library/svelte';import { afterEach, expect, it } from 'vitest';import BookCard from './BookCard.svelte';
afterEach(cleanup);
it('displays the supplied book rather than a fixed sample', () => { render(BookCard, { props: { book: { title: 'River Maps', author: 'A. Vale', available: false } } }); expect(screen.getByRole('heading', { name: 'River Maps' })).toBeInTheDocument(); expect(screen.getByText('By A. Vale')).toBeInTheDocument(); expect(screen.getByText('On loan')).toBeInTheDocument();});Run from the root of the practice application (practice/), where compose.yaml is located:
docker compose -p wsd-practice-test -f compose.yaml -f compose.test.yaml run --build --rm --no-deps client deno task testUse the practice application’s unique Compose test project name from Part 1, distinct from the marketplace project’s Compose test project name. The test deliberately uses a different book from the page. Hardcoding the worked-example title would fail. The query targets a heading’s accessible name, not a generated CSS class or exact serialization of the entire DOM.
Cleanup removes mounted components between tests. A passing isolated test does not prove that the page passes the right props; a browser flow can check that separate boundary. See the Svelte testing guidance and Svelte Testing Library API.
A rerendered card
0 / 5 points
A test renders a talk card with cancelled: false and checks Scheduled.
Which additional check most directly detects a card that never updates after
its first prop value?
Observe a changed prop
Add a second test using the testing library’s rerender result. Start with available: false, rerender with available: true, then assert Available. Await rerender before the assertion.
This checks that output follows the current input rather than only the initial value. A component that stores an unnecessary snapshot of its prop can pass the first test and fail this one.
Compose a session card
Let’s use composition in a different interface. A conference session has its own speaker, duration and cancellation state. Create a card and a smaller speaker component, and let their props determine what appears. The grader supplies varied props rather than using only the example shown in the handout.
Compose a conference session
0 / 20 points
Task
Complete src/lib/SessionCard.svelte and src/lib/SpeakerBadge.svelte.
SessionCard receives a session prop with title, speaker,
durationMinutes, and cancelled. The supplied types describe valid input;
this task does not fetch or validate network data.
Requirements
Render an article with the supplied title as a level-two heading, the duration
as <number> minutes, and Scheduled or Cancelled according to the flag.
Cancelled sessions still show their title, speaker, and duration. Compose the
speaker through SpeakerBadge, whose speaker string prop is displayed as
Speaker: <name>.
The output must follow changed props, not only the first values. Do not mutate the supplied object. The components must work with unfamiliar titles, durations, and speaker names; treat text resembling HTML as text.
Check and submit
Use the component test in the chapter as a model for a different session and a
rerender. Submit only the two named components at their src/lib/ paths.
The grader supplies the exercise environment; no live Run preview is
available. The four checks award 5 points each: ordinary content, cancellation,
changed props, and the speaker component’s own varied-input contract.
Check Your Understanding
- What does a parent supply to a component, and what remains the component’s responsibility?
- Why is a prop type not runtime validation?
- When does a new component boundary clarify the design, and when might it add noise?
- Why does the example test use a book that the worked page never displayed?
- What additional behavior does a rerender test establish?