Introduction - What is Svelte?
Svelte is a (not so) new framework for building User Interfaces. It borrows some ideas from its more popular peers, like React and Vue.js, but it brings its own ideas into the mix, in order to maximize efficiency and performances.
Svelte is a radical new approach to building user interfaces. Whereas traditional frameworks like React and Vue do the bulk of their work in the browser, Svelte shifts that work into a compile step that happens when you build your app.
Svelte is a compiler rather than a framework. It “runs at build time, converting your components into highly efficient imperative code”, meaning that when your application runs it relies only on vanilla JavaScript, without any overhead provided by the framework or by Virtual DOM library. The framework disappears when your code is launched in production.
In practical terms this means:
- significantly smaller output bundle (its React counterpart can be up to 40% larger)
- faster execution time. Svelte doesn’t use a Virtual DOM to track what component needs to change, but it skips this intermediate step and surgically updates only the relevant DOM parts.
Warning: I didn’t do any benchmark to backup those assumptions, except my own experience, therefore you should take it with a grain of salt.
The smaller bundle is also achieved by removing redundant boilerplate, without sacrificing readability of the code. Rich Harrs, the creator of Svelte, is well aware that less code means less bugs, lower cognitive effort required to understand it and more time for other activities.
It’s widely acknowledged that project development time and bug count grow quadratically, not linearly, with the size of a codebase.
If you want to know more, you should definitely check out this brilliant talk by Rich Harris about the motivations behind Svelte. It contains also a bunch of cool demos that are also available online.
Svelte has been around for a few years now, but with the arrival of Svelte 3 it has finally reach the maturity and it’s starting to look like a serious contender to React and Vue.js, when it comes to choosing a tool to build UIs for the web.
One app, four frameworks:
— Rich Harris (@Rich_Harris) July 8, 2019
• Svelte: 1,814 source bytes of component code
• Vue: 2,501
• React: 3,332
• Angular: 3,905 (across six files)
(numbers from `wc -c` applied to App.[svelte|vue|tsx] or, in Angular's case, the app directory) https://t.co/ePbMCc5l1Y
The official Svelte tutorial is where you should start if you want to explore most of its concepts interactively. The documentation is good as well, so you should definitely check it out too.
Wait, why another JS framework?
Yes, You Should Learn Vanilla JavaScript Before Fancy JS Frameworks. And yes, a decade ago things were surprisingly simpler: just an index.html
file, some HTML tags (marquee
anyone?), the most basic CSS you can think of and a sparkle of JavaScript.
Right now the concepts that a frontend developer needs to grasp are much more broader: Virtual DOM, reconciliation, ES6 and beyond, one-way data flow, CSS-in-JS, the hell that is Webpack configuration 😩, just to name a few.
That’s why I think a library like Svelte could help beginners to jump into modern JavaScript development, providing that excitement level from component-driven development and HOT reloading, but at the same time without hiding the fundamentals of JavaScript behind fancy abstractions.
Build a pagination component with Svelte
Enough talking about numbers and concepts. Let’s try building something and see what the code looks like. In this section I’m going to build this pagination component right here 👇 It’s interactive so you can immediately try it out. (Source code on GitHub)
Data will be fetched from a Laravel backend via AJAX requests. Understand how pagination works in Laravel is not essential for the purpose of this post, but you might want to get familiar to the JSON structure that Laravel returns for paginated data, since this will essentially be the state of our application. Here’s the example from the documentation:
{
"total": 50,
"per_page": 15,
"current_page": 1,
"last_page": 4,
"first_page_url": "http://laravel.app?page=1",
"last_page_url": "http://laravel.app?page=4",
"next_page_url": "http://laravel.app?page=2",
"prev_page_url": null,
"path": "http://laravel.app",
"from": 1,
"to": 15,
"data":[
{
// Result Object
},
{
// Result Object
}
]
}
Easy, isn’t it?
Notice:
I’m not going into details of how configure Webpack/Rollup/Parcel/… to bundle a Svelte application. You should follow the documentation to know what the easiest way to get started. Unlike Vue.js and React, you cannot include Svelte from a CDN using a <script>
tag (remember that it is a compiler). If you want to use it in a Laravel application the easiest way is probably to include it in Laravel Mix using the laravel-mix-svelte package.
Before diving in, here is what we will cover in this section:
- split the application in more than one component
- passing props to components
- handle events
- slots
- animations and transitions
- reactive statements
The following is an overview of how the application is splitted in sub-components. Data will flow from the <App>
component down to its children using props and children will communicate with the parent using events. Those concepts are well established in any modern JavaScript framework and Svelte embodies them as well.
<App>
<Table>
<Row></Row>
<Row></Row>
<Row></Row>
...
</Table>
<Pagination></Pagination>
</App>
Notice: In this post I’m not focusing on the CSS code, but you can take a look at the repository if you’re interested. Just remember that Svelte scopes CSS by default.
Let’s start with the main.js
file, the entry point of our application.
import App from './App.svelte';
const app = new App({
target: document.getElementById('app')
});
export default app;
Here we import the <App>
component and we render it on the page, inside a tag with id="app"
. This is not different from how React and Vue.js link to the actual DOM.
App
Next we can start devloping the <App>
component. Components are the building blocks of Svelte applications. They are written into .svelte
files, using a superset of HTML. This means that every Svelte component can have a <script>
tag, a <style>
tag and some markup, but all three sections are optional.
First let’s define the state of the component, along with some default values. In Svelte any locally declared variables is “reactive” and accessible from markup. There’s no framework specific syntax to learn for state management, so no Angular $scope
, no React this.state
, and no Vue.js data
:
<script>
let current_page = 1;
let from = 1;
let to = 1;
let per_page = 1;
let last_page = 1;
let total = 0;
let rows = [];
let loading = true;
</script>
As you can see, we are just replicating most of the fields of the JSON object returned by our backend. We’re going to update those values after each AJAX request, so the state of our application will always be in sync with the latest results.
Now we can import the axios library to perform AJAX requests and add the changePage
function that will be triggered at each page change.
<script>
import axios from 'axios';
[...]
function changePage(params) {
axios.get('/api/users', {
params: params
})
.then(function (response) {
current_page = response.data.current_page;
from = response.data.from;
to = response.data.to;
per_page = response.data.per_page;
last_page = response.data.last_page;
total = response.data.total;
rows = response.data.data;
})
.catch(error => {
console.error(error);
})
.finally(() => {
loading = false;
});
}
changePage({ page: 1 });
</script>
Again, note that there’s no framework specific syntax for declaring methods. The changePage
function is immediately accessible from the markup so we can later connect it from the Pagination
component. At the bottom you can see that we are calling it immediately changePage({ page: 1 });
to fetch the initial data. This function will be called as soon as the component gets executed, before being mounted into the DOM. If you want to fetch data as soon as the component is mounted you can use Svelte’s own onMount
hook, that works exactly like React’s componentDidMount()
or Vue.js beforeMount()
lifecycle functions.
Now it’s time to look at the markup of our component:
{#if loading}
<span>Loading</span>
{/if}
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Surname</th>
</tr>
</thead>
<tbody>
{#each rows as row}
<tr>
<td>{row.id}</td>
<td>{row.name}</td>
<td>{row.surname}</td>
</tr>
{:else}
<tr>
<td colspan="100%">
<h5 class="text-center">There are no Users here.</h5>
</td>
</tr>
{/each}
</tbody>
</table>
The markup and the template syntax are both really simple. Here we are introducing the {#if ...
syntax, that Svelte uses to conditionally render markup, and the {#each ...}
syntax used to iterate over list of values.
Notice:
take always into account empty states, as they are essential in any well-designed User Interface. In this case we are using the {:else}
statement to inform the user that there is no data when the rows
array is empty.
People who are already familiar with React or Vue.js will immediately notice another key feature here: this component has multiple root nodes, because the loading span
tag and the table
tag can appear simultaneously 😱. For those unfamiliar with this concept, if you try to create a Vue template without a root node, such as this:
<template>
<div>Node 1</div>
<div>Node 2</div>
</template>
you’ll get a compilation and/or runtime error, because templates must have a single root element. React had the same limitation, but it provided an answer in version 16.2 with a feature called fragments. Vue.js users can solve this problem using a plugin, but still this limitation is an issue in a few particular cases, which frustrates beginners as well as experienced developers.
Svelte doesn’t have this limitation because, as we already said, it doesn’t use a diffing algorithm at runtime 🎉.
Before moving on we can extract a few parts of the markup into their own components to make things look cleaner. The loading indicator and the table rows are good candidates for this.
<script>
import Overlay from './Overlay.svelte';
import Row from './Row.svelte';
[...]
</script>
{#if loading}
<Overlay />
{/if}
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Surname</th>
</tr>
</thead>
<tbody>
{#each rows as row}
</Row {row}>
{:else}
<tr>
<td colspan="100%">
<h5 class="text-center">There are no Users here.</h5>
</td>
</tr>
{/each}
</tbody>
</table>
Overlay
The Overlay
component allows us to cover the table with a nice transparent overlay when the AJAX request is loading, so the user is prevented from clicking again triggering new requests. Using this component we have the chance to introduce Svelte’s built in transitions, which are really easy to integrate and make your application look way more professional. In this case we are using the fade transition.
<script>
import { fade } from 'svelte/transition';
</script>
<div class="overlay" transition:fade="{{delay: 0, duration: 300}}">
<p>Loading...</p>
</div>
Row
The <Row>
component is only used to isolate the table’s row markup, but we can take it as an excuse to introduce how props are defined in Svelte.
Svelte uses the export
keyword to mark a variable declaration as a property or prop, which means it becomes accessible to consumers of the component, so they can inject their own values. Note that this notation </Row {row}>
is equivalent to </Row row={row}>
.
<script>
export let row;
</script>
<tr>
<td>{row.id}</td>
<td>{row.name}</td>
<td>{row.surname}</td>
</tr>
Pagination
It’s time to introduce our main pagination component, which will allow the user to jump to any page of our paginated data. First of all we need to create a new Pagination.svelte
file and include it in our main App
.
|
|
As you can see, the Pagination
component is included in the DOM only when the total number of rows is higher than the number of rows that we want to display on the page. We will then pass most of our state down to the component via props.
We also have a special directive to listen to events: when the component emits a change
event with a new page number, we need to fetch new data via AJAX, calling the changePage
method in the parent. ev.detail
on Line 19
can be a bit confusing, but we will clear its meaning later on.
Here’s how the Pagination
component looks like:
|
|
There’s a lot to dissect here.
First of all this component is completely stateless because it’s state is determined by props provided from the outside (again declared at the top of the file). Inside the markup we print all the available pages using a handy range
function (L13
), which is used inside the {#each}
loop.
Since in Svelte attribute values can contain JavaScript expressions, we can dynamically toggle classes (active
, disabled
), based on our state, using ternary expressions.
When the user clicks on a page number or on one of the previous/next arrows, the click
DOM event is captured by Svelte (see lines 31
, 37
and 41
) and delegated to the changePage
function, with the corresponding page that needs to be loaded. The function checks that the requested page is different than the current one and finally dispatches the change
event to the parent. Event dispatchers are functions that can take two arguments: name
and detail
. In this case the detail
property carries the new page number, that’s why in the parent we use this syntax on:change="{(ev) => changePage({page: ev.detail})}"
.
Bonus
The application already looks good at this point, but there’s some space of improvement.
Let’s add for example a button for each row that allow us to show more details about the user. We are going to use a modal window to show the email address and an Emoji that represents the current mood of the user.
First of all we need to add a new column to the heading of the table:
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Surname</th>
<th>Details</th>
</tr>
</thead>
[...]
</table>
Next we can create a new file Modal.svelte
for our modal window. This component is part of the official Svelte examples, so you can play with it directly on the Svelte REPL.
<script>
import { fade } from 'svelte/transition';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
</script>
<div class="modal-background" transition:fade="{{delay: 0, duration: 300}}"></div>
<div class="modal" on:click="{() => dispatch('close')}" transition:fade="{{delay: 0, duration: 300}}">
<div class="modal-header">
<button class="close" on:click="{() => dispatch('close')}">×</button>
<slot name="header"></slot>
</div>
<slot></slot>
<div class="modal-footer">
<button on:click="{() => dispatch('close')}">Close</button>
</div>
</div>
Nothing new here, except for the slot tag, which brings the same content distribution API inspired by the Web Components spec draft and made popular by Vue.js. This syntax allows us to inject content into different parts of the component from the outside.
Finally we need to update our Row
component in order to add the Details button and the modal itself.
|
|
In this case the Modal
component is attached to the DOM only when the showModal
boolean is true
, after clicking the button. The header of the modal window is injected using the slot="header"
attribute on the title and the default slot is injected with the rest of markup.
The only curious notation in this snippet is the dollar sign on line 8
. Essentially this tells Svelte to define a reactive fullname
property, which “is evaluated immediately before the component updates, whenever the values that they depend on have changed”. This works exactly like Vue.js computed properties.
The Svelte syntax is again valid JavaScript syntax known as labeled statement, even thought it’s far from being widely used.
Conclusions
This post ends here, but there a lot of features of Svelte 3 that we didn’t touch, to name a few:
Svelte is quite an interesting library and I’m definitely looking forward to use it again. I’m not going to make comparisons against Vue.js and React and pick a winner because each tool has its own pros and cons, but I have to admit that I was really surprised by its speed, its intuitiveness and its overall cleaner approach.
What is good for developers is ultimately good for users.
Wrapping up
The source code for this tutorial is available here svelte-pagination.
Thanks for reading and stay tuned!