Stop Overcomplicating Web Development - Try Svelte
Emmanuil B.
Posted on March 20, 2022
Svelte was rated as the most loved web framework by developers in 2021 (link). So what is Svelte and why is it so loved?
Svelte is a rather unique javascript framework which aims to be 'truly reactive' and help developers 'write less code'.
It has the awesome feature of creating tiny code bundles by making the framework itself 'disappear' in a compilation stage, shipping optimised vanilla JavaScript code rather than using large and complex libraries loaded at run time.
In my opinion, this is the game changer that makes Svelte standout from other JavaScript frameworks. A tiny bundle size means a much faster load time, which seems to be the direction the web is taking as more and more data shows the benefits of a quick website. This compilation stage also removes the need for techniques such as the virtual DOM, used by React and Vue, further increasing the speed of a website.
Another feature is the absence of boilerplate. Svelte feels very close to standard web development, where components can look exactly like vanilla HTML. I’m sure this is a big reason why developers love this framework.
To introduce Svelte, let’s use the the poke api to create a simple single page app where users can select a pokemon, with a live search bar to filter through the list of all the pokemon. This will demonstrate all the core features of Svelte in a useful way. The full code can be found here
Table of Contents
- Installation
- Component Features
- Variables & Reactivity
- onMount & Async Fetching
- Reactive Declarations
- Loops
- Conditional Rendering
- Components & Props
- Custom Events
- Bind Forwarding
- Stores
- Final Notes
Installation
Let’s first install basic Svelte. To do this, run the following command
npx degit sveltejs/template new-svelte-project
This will copy the svelte starter template into your desired folder.
To enable typescript, go into your new svelte folder, and run
node scripts/setupTypeScript.js
Now all you need to do is install the necessary files by running
npm install
Component Features
A svelte component is a file ending with .svelte.
As you can see in App.svelte, a Svelte component can be pretty simple, containing three parts; html, a script tag to put your javascript, and a style tag to place css.
This is similar to Vue, just without the boilerplate code.
Clear the script and html contents of App.svelte, and let’s use the given css in the style tag.
Variables & Reactivity
Variables are created in the script tag.
We can create a string variable and display it in the DOM very easily by using curly braces {}.
<!-- For example -->
<script lang="ts">
let name: string = 'pokemon searcher'
</script>
<h1>{name}</h1>
To search through pokemon names, we’ll need an input field and a variable that holds the contents of that field.
To make the 'pokemonName' variable equal to the contents of an input field, we can use a special svelte keyword 'bind', which enables two way binding of the 'pokemonName' variable.
<!-- App.svelte -->
<script lang="ts">
let pokemonName: string = ''
</script>
<main>
<span>Search: </span>
<input type="text" bind:value="{pokemonName}" />
<h1>Pokemon: {pokemonName}</h1>
</main>
Now, typing into the input field changes the output of the pokemon title.
This bind keyword enables two way binding without using an 'onInput' function that changes the value of the ‘pokemonName’ variable like in React.
onMount & Async Fetching
For this sample app, we store pokemon names from the pokeapi in a variable as an array of strings.
We want to fetch the data and map it as soon as the component is rendered.
For this, we can use 'onMount', which is a svelte lifecycle function that runs after the component is first rendered to the DOM. Let’s use this to fetch from the pokeapi and map it into an array of pokemon names.
<!-- App.svelte - script tag -->
<script lang="ts">
import { onMount } from 'svelte'
let pokemonName: string = ''
// Fetch from api then store the mapped names.
let pokemonData: string[] = []
onMount(() => {
const setPokemonData = async (): Promise<void> => {
const rawPokemonData = await (
await fetch('https://pokeapi.co/api/v2/pokemon?limit=99')
).json()
pokemonData = rawPokemonData.results.map(
(p: { name: string; url: string }) => p.name
)
}
setPokemonData()
})
</script>
We now have a list of pokemon names in the 'pokemonData' array, which we can use in our simple project.
Reactive Declarations
For the live search feature, we need to have an array that holds the items filtered by the user input from the pokemon names.
Svelte has an awesome tool to deal with states that are derived from other properties, reactive declarations.
They look like this.
$: reactiveVar = otherVar * 2;
Now, 'reactiveVar' is a variable, but its value is computed every time the 'otherVar' variable changes (svelte runs the computation when the variables used in that computation changes).
We can make the variable that holds the filtered pokemon names into a reactive declaration. We’ll call this 'suggestions'.
<!-- App.svelte - bottom of script tag -->
<script>
// ...
let suggestions: string[]
$: suggestions =
pokemonName.length > 0
? pokemonData.filter(
(name) => name.includes(pokemonName)
)
: pokemonData
</script>
So, 'suggestions' is an array of pokemon names that includes the string entered in the input field.
The reactive assignment doesn’t work with typescript, so we can declare a 'suggestions' variable normally to preserve type checks.
Loops
We’ll want to display the contents of this 'suggestions' array on the page, and we can do this by using a svelte loop. Svelte has a special keyword 'each' that enables us to display DOM elements for each item in the given iterable.
To display each pokemon name, simply use the each keyword to loop over the 'pokemonData' variable.
<!-- App.svelte - html -->
<main>
<span>Search: </span>
<input type="text" bind:value="{pokemonName}" />
{#each suggestions as suggestion}
<h2>{suggestion}</h2>
{/each}
</main>
As we type into the input field, we can see the list of suggestions change. Pretty cool for such simple code.
Conditional Rendering
Svelte has other keywords. Another useful one is #if.
The #if keyword allows for conditional logic.
For example, we can render a loading screen while we fetch the data from the pokeapi.
<!-- App.svelte - html -->
<main>
{#if pokemonData && pokemonData.length > 0}
<span>Search: </span>
<input type="text" bind:value="{pokemonName}" />
{#each suggestions as suggestion}
<h2>{suggestion}</h2>
{/each}
{:else}
<h2>Loading...</h2>
{/if}
</main>
Components & Props
Props are used to pass data from one component to another. This is fairly standard for frameworks, and the syntax for this is very simple.
To signal that a component accepts a prop, an export keyword is used for a variable.
<script lang="ts">
export let stringProp: string
</script>
Now, to pass the value for 'stringProp', simply use the name of the exported variable when writing the component.
<script lang="ts">
import NewComponent from './NewComponent.svelte'
</script>
<NewComponent stringProp="prop value" />
For our app, let’s create a component for each suggestion.
Create a new file 'Suggestion.svelte' in src/, and simply accept and display a 'suggestion' prop.
<!-- Suggestion.svelte -->
<script lang="ts">
export let suggestion: string
</script>
<div class="suggestion">{suggestion}</div>
<style>
.suggestion {
font-size: 1.25rem;
}
</style>
Now, we can import this component and use it in our #each loop.
<!-- App.svelte - top of script tag -->
<script lang="ts">
import Suggestion from './Suggestion.svelte'
// ...
// ...
</script>
<!-- App.svelte - html -->
<main>
{#if pokemonData && pokemonData.length > 0}
<span>Search: </span>
<input type="text" bind:value="{pokemonName}" />
{#each suggestions as suggestion}
<Suggestion suggestion="{suggestion}"/>
{/each}
{:else}
<h2>Loading...</h2>
{/if}
</main>
This is pretty pointless at the moment, so let’s add some logic to the 'Suggestion' component in the form of events.
Custom Events
Custom events can be dispatched from one component to another. This allows us to have parent-child communication.
For our app, we want to be able to click on a suggestion to select our pokemon. We can do this by dispatching a custom event from the 'Suggestion' component to the App component, and then setting the value of a variable which holds our chosen pokemon.
First, create the new 'chosenPokemon' variable, and display it on the screen in App.svelte.
<!-- App.svelte - bottom of script tag -->
<script lang="ts">
// ...
// ...
let chosenPokemon: string = ''
</script>
<!-- App.svelte - html -->
<main>
{#if pokemonData && pokemonData.length > 0}
<h1>Chose Your Pokemon</h1>
<h2>Chosen Pokemon: {chosenPokemon}</h2>
<div>
<span>Search: </span>
<input type="text" bind:value="{pokemonName}" />
{#each suggestions as suggestion}
<Suggestion suggestion="{suggestion}"/>
{/each}
</div>
{:else}
<h2>Loading...</h2>
{/if}
</main>
Now, in Suggestion.svelte, we can dispatch a custom 'chosePokemon' event when clicking on a suggestion.
To create a custom event, we need to import the ‘createEventDispatcher’ from svelte.
<!-- Suggestion.svelte - script tag -->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
export let suggestion: string
// Function to dispatch a custom event.
const dispatch = createEventDispatcher()
const chosePokemon = (): void => {
dispatch('chosePokemon', {
pokemon: suggestion
})
}
</script>
We now have a 'chosePokemon' function that dispatches a custom 'chosePokemon' event to the parent component.
To call this function when clicking on a suggestion, we need to use the svelte 'on:click' event like this.
<!-- Suggestion.svelte - html -->
<div class="suggestion" on:click="{chosePokemon}">
{suggestion}
</div>
Back in the App.svelte file, we can handle this custom event by using the 'on:(event name)' syntax.
<!-- App.svelte - 'Suggestion' component in html -->
<Suggestion
suggestion="{suggestion}"
on:chosePokemon="{(e) => {
chosenPokemon = e.detail.pokemon
}}"
/>
This handler sets the value of the chosenPokemon variable to be equal to the pokemon name passed in the custom event (located in the 'detail' property).
When we click on a suggestion, that pokemon name is shown.
I've set the 'chosenPokemon' variable this way to introduce custom events, however, there is a much cleaner and easier way of doing this: bind forwarding.
Bind Forwarding
We saw how the bind keyword was used to set up two way binding when creating an input field, but this keyword can also be used across our components.
In App.svelte, we can replace the chosePokemon event handler with a bind keyword on a chosenPokemon prop.
<!-- App.svelte - 'Suggestion' component in html -->
<Suggestion suggestion="{suggestion}" bind:chosenPokemon />
And in the 'Suggestion' component we can accept this prop and make the 'on:click' function simply set this 'chosenPokemon' variable.
<!-- Suggestion.svelte -->
<script lang="ts">
export let suggestion: string
export let chosenPokemon: string = ''
</script>
<div
class="suggestion"
on:click="{() => chosenPokemon = suggestion}"
>
{suggestion}
</div>
We now have the same functionality as before using a fraction of the code.
Stores
I want to wrap things up by introducing stores.
With Svelte, we don’t need to use an external library like Redux to have a central store, it comes with the framework.
Fundamentally, a store is an object with a subscribe method that allows our Svelte components to be notified of any changes to the store value. Svelte defines 2 different types of stores, a writable store, and a readable store. As the names suggest, a writable store allows reads and writes, while a readable store only allows a read.
For our app, we’ll send the 'pokemonData' variable into the store. Since this variable simply gathers and stores the pokemon data, we’ll use a readable store.
First, we need a new file in the src folder (I’ll name it ‘stores.ts’).
We can import the readable store function from svelte, along with the required types.
// stores.ts
import { readable, Readable, Subscriber } from 'svelte/store'
A readable function, as its first argument, takes the store’s initial value, and, as its second argument, a 'start' function.
This 'start' function is called when the store gets its first subscriber, so it is where we’ll retrieve our api data.
The function receives a 'set' callback function, which is used to set the value of the store, and returns a 'stop' function which is called when the last subscriber unsubscribes (where we can perform some cleanup).
In our store, we can simply copy the contents of our 'setPokemonData' function, but instead of assigning the value of 'pokemonData', we call the 'set' function.
import { readable, Readable, Subscriber } from 'svelte/store'
const setPokemonData =
async (set: Subscriber<string[]>): Promise<void> => {
const rawPokemonData = await (
await fetch('https://pokeapi.co/api/v2/pokemon?limit=99')
).json()
set(
rawPokemonData.results.map(
(p: { name: string; url: string }) => p.name
)
)
}
// Export the new store 'pokemonData' variable.
export const pokemonData: Readable<string[]> =
readable([], (set) => {
setPokemonData(set)
return () => set([])
})
That’s it. We now have a central store holding our pokemon names in 'pokemonData'.
To use our store, we need to import the 'pokemonData' variable from our store file.
We can then use the special svelte '$' symbol to reference the store's value.
<!-- App.svelte -->
<script lang="ts">
import { pokemonData } from './stores.js'
import Suggestion from './Suggestion.svelte'
let pokemonName: string = ''
let suggestions: string[]
// $pokemonData instead of pokemonData
$: suggestions =
pokemonName.length > 0
? $pokemonData.filter((name) =>
name.includes(pokemonName)
)
: $pokemonData
let chosenPokemon: string = ''
</script>
<main>
{#if $pokemonData && $pokemonData.length > 0}
<h1>Chose Your Pokemon</h1>
<h2>Chosen Pokemon: {chosenPokemon}</h2>
<div>
<span>Search: </span>
<input type="text" bind:value="{pokemonName}" />
{#each suggestions as suggestion}
<Suggestion
suggestion="{suggestion}"
bind:chosenPokemon
/>
{/each}
</div>
{:else}
<h2>Loading...</h2>
{/if}
</main>
Our app works the same, but our api data is now centrally stored and can be used in any component.
Now, while svelte has writable and readable stores, anything that sticks to the svelte store 'contract' and implements the subscribe method is a store.
This means that stores are very flexible and can be tailored to your needs. You can even create a store in another language, like Rust, as shown here.
Final Notes
Svelte stands out in the cluttered world of JavaScript frameworks by not compromising user experience for developer experience or vice versa.
Svelte provides fantastic tools to make developing apps easy, while it’s compiler results in a tiny package for end users, reducing download times significantly.
Svelte has been among the most loved frameworks for a while now, even as its usage grows, a potential sign that it will become as big as Vue or React. This coincides with the recent push towards a more performant web, moving away from crazy large JavaScript packages served to the client towards server-side or hybrid rendering.
The Svelte team is now working on SvelteKit, which is Svelte’s version of Next.js, which you can learn about here.
If you enjoyed this article, please consider sharing it.
Checkout my github and other articles.
Posted on March 20, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.