Svelte 4: test reactive dependencies and local transitions

Reproduce a stale filter caused by a hidden dependency, then observe which nested-block changes trigger local and global transitions.
Test the behavior a syntax migration can preserve incorrectly
Svelte 4 was announced on 22 June 2023 as a maintenance release. This historical example focuses on two behaviors worth testing in an older component: which state a reactive statement sees, and which block owns a transition. It uses the release’s let, reactive assignment and on:click syntax rather than Svelte 5 runes.
The examples are for a maintainer who already understands Svelte components. Both compile with Svelte 4.0.0 in a downloadable browser fixture. A local name filter and a nested details panel provide observable results without an API, account or database. They do not establish that an application upgrade is complete.
Make the query an explicit dependency
The reactive assignment calls filterNames with both names and query. The compiler can see those references in the statement. In the browser test, entering mixed-case text with surrounding spaces reduces the list to Grace. A non-matching query removes the list and displays No matching names. Clearing the field restores all three names.
The server output already includes the full initial list. During hydration, the fixture retains a reference to the original input and verifies that the same node remains. Filtering after hydration then verifies that the input binding works, rather than treating matching initial HTML as sufficient evidence.
<script>
const names = ['Ada', 'Grace', 'Linus'];
let query = '';
function filterNames(items, search) {
const normalized = search.trim().toLowerCase();
return items.filter((name) =>
name.toLowerCase().includes(normalized)
);
}
$: matches = filterNames(names, query);
</script>
<label>
Filter names
<input bind:value={query} />
</label>
{#if matches.length > 0}
<ul>
{#each matches as name (name)}
<li>{name}</li>
{/each}
</ul>
{:else}
<p>No matching names.</p>
{/if}
A helper can hide the dependency it reads
The altered filter removes search from the helper’s arguments and reads query inside the helper instead. The reactive call becomes filterNames(names). The input still displays Grace after typing, but the list retains Ada, Grace and Linus. The two displayed values now disagree because the reactive statement does not see query as a dependency.
Pass query explicitly to restore the dependency. Renaming the helper or adding a comment cannot make an indirect read visible to this compile-time model. I recommend keeping a failing input case beside this kind of refactor: an initial render can look correct even when subsequent changes never recalculate the result.
The fixed names array is deliberate. Its unique entries make the keyed list unambiguous, and the filter has no asynchronous response ordering to manage. This example does not test server search, pagination, duplicate keys or locale-specific name matching.
Observe the block that triggers the transition
Svelte 4 made transitions local by default. The details paragraph below belongs to the inner detailsOpen block. With the panel open, toggling details off emits outrostart and outroend. Toggling details on emits introstart and introend. The fixture captures those actual DOM events rather than inferring animation from a screenshot.
Closing the outer panel removes the same paragraph but produces no inner transition events in the default local version. Reopening the panel also produces none. The data state and final markup can therefore be right while the animation differs from an older expectation.
<script>
import { fade } from 'svelte/transition';
let panelOpen = true;
let detailsOpen = true;
</script>
<button type="button" on:click={() => panelOpen = !panelOpen}>
Toggle panel
</button>
<button type="button" on:click={() => detailsOpen = !detailsOpen}>
Toggle details
</button>
{#if panelOpen}
<section aria-label="Example panel">
{#if detailsOpen}
<p transition:fade={{ duration: 150 }}>Example details</p>
{/if}
</section>
{/if}
Choose global behavior deliberately
The comparison variant changes only the directive to transition:fade|global. Closing its outer panel now emits outrostart and outroend for Example details. The modifier changes which block changes are allowed to trigger the transition. It is a choice about the interaction, not a general upgrade repair.
The fixture also closes the panel, toggles details while it is absent, and reopens it. The section is present with no details paragraph. That assertion checks attachment and content because an empty section can have no visible area. Testing only whether a section looks visible would confuse layout with the component state.
I recommend local behavior when the details animation should follow its own control. Use global behavior when animating ancestor removal is part of the intended interaction. The fixed fade in this fixture does not implement a reduced-motion preference, focus restoration or a dialog lifecycle. Those need their own design and verification in a production component.
Reproduce before drawing broader conclusions
Download and extract the archive, then run python3 frontend/scripts/verify-svelte-4.py from its root with Docker and Python 3 available. The expected result contains eleven checks and two altered versions. Image construction downloads locked packages; execution uses a loopback browser server with external networking disabled and no application credentials.
The fixture records compiler and runtime package 4.0.0, Node 24.20.0 and Chromium 153.0.8010.12. These are reproduction tools for historical behavior. The old migration guide’s Node 16 minimum is a release-time constraint, not advice to deploy an obsolete runtime today.
The release announcement separately describes smaller package downloads and generated hydration code. Neither measurement is reproduced here. These tests establish filtering and transition behavior, not installation speed or browser-bundle savings. I recommend measuring your own routes before making a performance claim about an upgrade.
Reproduce the examples
Exact filter and transition components, locked compiler and browser, eleven checks and two altered versions.
Download the source and tests (.zip)
