Earlier this month, Svelte version 4 was released to the public, bringing some highly anticipated features and fixes to typing, transitions, custom elements, stores, preprocessing, and more. However, the most significant improvements for developers lie in smaller and faster hydration code, as well as a significantly reduced package size.
While project managers will appreciate the smaller package size and improved hydration code, developers will find the other changes even more enticing. In our six months of experience with Svelte and SvelteKit, the announced features and fixes align perfectly with our needs, enhancing our development experience. So, let’s begin this analysis from the start.
The most notable change for us is the complete overhaul of Custom Elements, which used to be one of the more challenging aspects of Svelte, requiring numerous workarounds. With this overhaul, one of the most significant changes is that components can now be used both as custom elements and regular components. This allows us to create a single wrapper custom element and use regular Svelte components inside it, resolving many issues during development and usage of Custom Elements with Svelte or SvelteKit.
One of the quirkiest aspects of Custom Elements in pre-4.0 Svelte was how the lifecycle events functioned differently from normal Svelte components. For example, the onDestroy function didn’t trigger for the Custom Element when removed from the DOM because $destroy wasn’t called. Additionally, using props inside the onMount function was a major headache before this change.
The new features for Custom Elements also include compiling components with injected styles, emitting custom events from the Custom Element to the parent application, the option to encapsulate the Custom Element, and a change in `<svelte:options>`. Now, instead of `tag=”…”`, it uses `customElement={…}`, which can also take an object to define the Custom Element’s behavior.
Another notable change was made to transitions, which are now local by default. This addresses issues commonly encountered with page transitions or conditional transitions. Local transitions are only triggered when the associated block is created or destroyed, not when parent blocks are created or destroyed.
{#if x}
{#if y}
<p transition:fade>fades in and out when x or y change</p>
<p transition:fade|local>fades in and out only when y changes</p>
{/if}
{/if}
Now local
is switched out for global
which will reduce the need for piping an additional modifier.
{#if x}
{#if y}
<p transition:fade>fades in and out only when y changes</p>
<p transition:fade|global>fades in and out when x or y change</p>
{/if}
{/if}
Also, stores received a delightful little upgrade that enables the implementation of more complex logic. This is accomplished by allowing the setup callback in readable and writable stores to be in the form of `(set, update) => { }` instead of just `(set) => { }`. Furthermore, the value-deriving callback in the `derived` function can now accept an optional third parameter, `derived`, in addition to the optional second parameter, `set`. However, it’s worth noting that custom stores now need to pass an `update` parameter to the start callback alongside the `set` parameter, which is a breaking change.
A notable improvement, although somewhat niche, is the change in preprocessor ordering. In Svelte 4, you are no longer restricted to running markup, script, and style preprocessors in a specific order. Instead, you have the freedom to group the preprocessors as you see fit or based on your specific requirements.
On the other hand, a few breaking changes have been introduced in relation to type safety. Specifically, improvements have been made to `onMount`, `createEventDispatcher`, `Action`, and `ActionReturn`. In our experience, `createEventDispatcher` has worked wonderfully, and now the generic that defines the events and payloads can specify if the payload is optional or available.
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher<{
optional: number | null;
required: string;
noArgument: never;
}>();
// Svelte version 3
dispatch('optional');
dispatch('required'); // I can still omit the detail argument
dispatch('noArgument', 'surprise'); // I can still add a detail argument
// Svelte version 4 using TypeScript strict mode
dispatch('optional');
dispatch('required'); // error, missing argument
dispatch('noArgument', 'surprise'); // error, cannot pass an argument
Lastly, the change to the #each
block while being the smallest might just be the most used in the future. In Svelte 4 we can finally allow the #each
block to iterate over Array like iterables, like Set or Map which will reduce written code even more for already tiny Svelte apps.
<script>
const a = new Map([
['key1', 'value1'],
['key2', 123],
['key3', '456']
]);
</script>
{#each a as [key, value]}
{`${key} ${value}`}
{/each}
It should come as no surprise that we are thrilled about Svelte 4 and all of its new features and enhancements. This has only been a brief overview of the latest release, so make sure to stay tuned for our next update, where we delve into Svelte’s approach to handling Custom Elements and how we incorporate it into our projects. Until then, check out previous blog posts about Svelte – in the first one we discussed how Svelte manages state within the component hierarchy and in the second blog post we look into all of Svelte’s peculiarities and features as our team discovered them after first six months of using Svelte.
Josip Ante Cindrić

Software Engineer who joined our team because it seemed like a great opportunity to develop great things while growing as a person in a healthy environment. He loves dogs and board games.